Beispiel #1
0
    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
Beispiel #2
0
    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)
Beispiel #3
0
    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
Beispiel #4
0
    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