def test_inv_dict(self): """Test _inv_dict correctly inverts a dictionary""" test_data = {"c": 8, "d": (0, 0.65), "e": "hi", "f": None, "g": 8} res = pu._inv_dict(test_data) expected = {8: {"g", "c"}, (0, 0.65): {"d"}, "hi": {"e"}, None: {"f"}} assert res == expected
def test_inv_dict_unhashable_key(self): """Test _inv_dict raises an exception if a dictionary value is unhashable""" test_data = {"c": 8, "d": [0, 0.65], "e": "hi", "f": None, "g": 8} with pytest.raises(TypeError, match="unhashable type"): pu._inv_dict(test_data)
def jacobian(self, args, kwargs=None, *, wrt=None, method="best", options=None): r"""Compute the Jacobian of the QNode. Returns the Jacobian of the parametrized quantum circuit encapsulated in the QNode. The Jacobian is returned as a two-dimensional array. The (possibly nested) input arguments of the QNode are :func:`flattened <_flatten>` so the QNode can be interpreted as a simple :math:`\mathbb{R}^m \to \mathbb{R}^n` function. The Jacobian can be computed using several methods: * Finite differences (``'F'``). The first-order method evaluates the circuit at :math:`n+1` points of the parameter space, the second-order method at :math:`2n` points, where ``n = len(wrt)``. * Analytic method (``'A'``). Analytic, if implemented by the inheriting QNode. * Best known method for each parameter (``'best'``): uses the analytic method if possible, otherwise finite difference. * Device method (``'device'``): Delegates the computation of the Jacobian to the device executing the circuit. Only supported by devices that provide their own method for computing derivatives; support can be checked by querying the device capabilities: ``dev.capabilities()['provides_jacobian']`` must return ``True``. Examples of supported devices include the experimental :class:`"default.tensor.tf" <~.DefaultTensorTF>` device. .. note:: The finite difference method is sensitive to statistical noise in the circuit output, since it compares the output at two points infinitesimally close to each other. Hence the 'F' method requires exact expectation values, i.e., ``analytic=True`` in simulator devices. Args: args (nested Iterable[float] or float): positional arguments to the quantum function (differentiable) kwargs (dict[str, Any]): auxiliary arguments to the quantum function (not differentiable) wrt (Sequence[int] or None): Indices of the flattened positional parameters with respect to which to compute the Jacobian. None means all the parameters. Note that you cannot compute the Jacobian with respect to the kwargs. method (str): Jacobian computation method, in ``{'F', 'A', 'best', 'device'}``, see above options (dict[str, Any]): additional options for the computation methods * h (float): finite difference method step size * order (int): finite difference method order, 1 or 2 Returns: array[float]: Jacobian, shape ``(n, len(wrt))``, where ``n`` is the number of outputs returned by the QNode """ # pylint: disable=too-many-branches,too-many-statements if not isinstance(args, Iterable): args = (args,) kwargs = kwargs or {} # apply defaults kwargs = self._default_args(kwargs) options = options or {} # Add the step size into the options, if it was not there already if "h" not in options.keys(): options = {"h": self.h, **options} if "order" not in options.keys(): options = {"order": self._order, **options} # (re-)construct the circuit if necessary if self.circuit is None or self.mutable: self._construct(args, kwargs) returns_samples = [ str(ob) for ob in self.circuit.observables if ob.return_type is ObservableReturnTypes.Sample ] if returns_samples: raise QuantumFunctionError( "Circuits that include sampling can not be differentiated. " "The following observables include sampling: {}".format("; ".join(returns_samples)) ) # check that the wrt parameters are ok if wrt is None: wrt = range(self.num_variables) else: if min(wrt) < 0 or max(wrt) >= self.num_variables: raise ValueError( "Tried to compute the gradient with respect to parameters {} " "(this node has {} parameters).".format(wrt, self.num_variables) ) if len(wrt) != len(set(wrt)): # set removes duplicates raise ValueError("Parameter indices must be unique.") # check if the method can be used on the requested parameters method_map = _inv_dict(self.par_to_grad_method) def inds_using(m): """Intersection of ``wrt`` with free params indices whose best grad method is m.""" return method_map.get(m, set()).intersection(wrt) # are we trying to differentiate wrt. params that don't support any method? bad = inds_using(None) if bad: # get bad argument name bad_var_names = { v.name for v in _flatten(self.arg_vars) if hasattr(v, "idx") and v.idx in bad } raise ValueError( "Cannot differentiate with respect to argument(s) {}.".format(bad_var_names) ) if method == "device": self._set_variables(args, kwargs) return self.device.jacobian( self.circuit.operations, self.circuit.observables, self.variable_deps ) if method == "A": bad = inds_using("F") # get bad argument name bad_var_names = { v.name for v in _flatten(self.arg_vars) if hasattr(v, "idx") and v.idx in bad } if bad: raise ValueError( "The analytic gradient method cannot be " "used with the argument(s) {}.".format(bad_var_names) ) # only variants of the analytic method remain method = self.par_to_grad_method elif method == "F": # use the requested method for every parameter method = {k: "F" for k in wrt} elif method == "best": # use best known method for each parameter method = self.par_to_grad_method else: raise ValueError("Unknown gradient method.") if "F" in method.values(): if options.get("order", 1) == 1: # the value of the circuit at args, computed only once here options["y0"] = np.asarray(self.evaluate(args, kwargs)) # In the following, to evaluate the Jacobian we call self.evaluate several times using # modified args (and possibly modified circuit Operators). # We do not want evaluate to call _construct again. This would only be necessary if the # auxiliary args changed, since only they can change the structure of the circuit, # and we do not modify them. To achieve this, we temporarily make the circuit immutable. mutable = self.mutable self.mutable = False # flatten the nested Sequence of input arguments flat_args = np.array(list(_flatten(args)), dtype=float) variances_required = any( ob.return_type is ObservableReturnTypes.Variance for ob in self.circuit.observables ) # compute the partial derivative wrt. each parameter using the appropriate method grad = np.zeros((self.output_dim, len(wrt)), dtype=float) for i, k in enumerate(wrt): par_method = method[k] if par_method == "0": # unused/invisible, partial derivatives wrt. this param are zero continue if par_method == "A": if variances_required: grad[:, i] = self._pd_analytic_var(k, flat_args, kwargs, **options) else: grad[:, i] = self._pd_analytic(k, flat_args, kwargs, **options) elif par_method == "F": grad[:, i] = self._pd_finite_diff(k, flat_args, kwargs, **options) else: raise ValueError("Unknown gradient method.") self.mutable = mutable # restore original mutability return grad
def jacobian(self, params, which=None, *, method='B', h=1e-7, order=1, **kwargs): """Compute the Jacobian of the QNode. Returns the Jacobian of the parametrized quantum circuit encapsulated in the QNode. The Jacobian can be computed using several methods: * Finite differences (``'F'``). The first order method evaluates the circuit at :math:`n+1` points of the parameter space, the second order method at :math:`2n` points, where ``n = len(which)``. * Analytic method (``'A'``). Works for all one-parameter gates where the generator only has two unique eigenvalues; this includes one-parameter qubit gates, as well as Gaussian circuits of order one or two. Additionally, can be used in CV systems for Gaussian circuits containing first- and second-order observables. The circuit is evaluated twice for each incidence of each parameter in the circuit. * Best known method for each parameter (``'B'``): uses the analytic method if possible, otherwise finite difference. .. note:: The finite difference method is sensitive to statistical noise in the circuit output, since it compares the output at two points infinitesimally close to each other. Hence the 'F' method requires exact expectation values, i.e., `analytic=True` in simulation plugins. Args: params (nested Sequence[Number], Number): point in parameter space at which to evaluate the gradient which (Sequence[int], None): return the Jacobian with respect to these parameters. None (the default) means with respect to all parameters. Note that keyword arguments to the QNode are *always* treated as fixed values and not included in the Jacobian calculation. method (str): Jacobian computation method, see above. Keyword Args: h (float): finite difference method step size order (int): finite difference method order, 1 or 2 shots (int): How many times the circuit should be evaluated (or sampled) to estimate the expectation values. Returns: array[float]: Jacobian matrix, with shape ``(n_out, len(which))``, where ``len(which)`` is the number of free parameters, and ``n_out`` is the number of expectation values returned by the QNode. """ # pylint: disable=too-many-statements # in QNode.construct we need to be able to (essentially) apply the unpacking operator to params if isinstance(params, numbers.Number): params = (params,) circuit_kwargs = pop_jacobian_kwargs(kwargs) if not self.ops or not self.cache: # construct the circuit self.construct(params, circuit_kwargs) sample_ops = [ e for e in self.circuit.observables if e.return_type is qml.operation.Sample] if sample_ops: names = [str(e) for e in sample_ops] raise QuantumFunctionError("Circuits that include sampling can not be differentiated. " "The following observable include sampling: {}".format('; '.join(names))) flat_params = np.array(list(_flatten(params))) if which is None: which = range(len(flat_params)) else: if min(which) < 0 or max(which) >= self.num_variables: raise ValueError("Tried to compute the gradient wrt. free parameters {} " "(this node has {} free parameters).".format(which, self.num_variables)) if len(which) != len(set(which)): # set removes duplicates raise ValueError("Parameter indices must be unique.") # check if the method can be used on the requested parameters mmap = _inv_dict(self.grad_method_for_par) def check_method(m): """Intersection of ``which`` with free params whose best grad method is m.""" return mmap.get(m, set()).intersection(which) bad = check_method(None) if bad: raise ValueError('Cannot differentiate wrt parameter(s) {}.'.format(bad)) if method in ('A', 'F'): if method == 'A': bad = check_method('F') if bad: raise ValueError("The analytic gradient method cannot be " "used with the parameter(s) {}.".format(bad)) method = {k: method for k in which} elif method == 'B': method = self.grad_method_for_par else: raise ValueError('Unknown gradient method.') if 'F' in method.values(): if order == 1: # the value of the circuit at params, computed only once here y0 = np.asarray(self.evaluate(params, **circuit_kwargs)) else: y0 = None variances = any(e.return_type is qml.operation.Variance for e in self.circuit.observables) # compute the partial derivative w.r.t. each parameter using the proper method grad = np.zeros((self.output_dim, len(which)), dtype=float) for i, k in enumerate(which): if k not in self.variable_deps: # unused parameter continue par_method = method[k] if par_method == 'A': if variances: grad[:, i] = self._pd_analytic_var(flat_params, k, **kwargs) else: grad[:, i] = self._pd_analytic(flat_params, k, **kwargs) elif par_method == 'F': grad[:, i] = self._pd_finite_diff(flat_params, k, h, order, y0, **kwargs) else: raise ValueError('Unknown gradient method.') return grad