Esempio n. 1
0
    def adjoint_jacobian(self,
                         tape,
                         starting_state=None,
                         use_device_state=False):
        """Implements the adjoint method outlined in
        `Jones and Gacon <https://arxiv.org/abs/2009.02823>`__ to differentiate an input tape.

        After a forward pass, the circuit is reversed by iteratively applying inverse (adjoint)
        gates to scan backwards through the circuit.

        .. note::
            The adjoint differentiation method has the following restrictions:

            * As it requires knowledge of the statevector, only statevector simulator devices can be
              used.

            * Only expectation values are supported as measurements.

            * Does not work for Hamiltonian observables.

        Args:
            tape (.QuantumTape): circuit that the function takes the gradient of

        Keyword Args:
            starting_state (tensor_like): post-forward pass state to start execution with. It should be
                complex-valued. Takes precedence over ``use_device_state``.
            use_device_state (bool): use current device state to initialize. A forward pass of the same
                circuit should be the last thing the device has executed. If a ``starting_state`` is
                provided, that takes precedence.

        Returns:
            array: the derivative of the tape with respect to trainable parameters.
            Dimensions are ``(len(observables), len(trainable_params))``.

        Raises:
            QuantumFunctionError: if the input tape has measurements that are not expectation values
                or contains a multi-parameter operation aside from :class:`~.Rot`
        """
        # broadcasted inner product not summing over first dimension of b
        sum_axes = tuple(range(1, self.num_wires + 1))
        dot_product_real = lambda b, k: self._real(
            qmlsum(self._conj(b) * k, axis=sum_axes))

        for m in tape.measurements:
            if m.return_type is not qml.operation.Expectation:
                raise qml.QuantumFunctionError(
                    "Adjoint differentiation method does not support"
                    f" measurement {m.return_type.value}")

            if m.obs.name == "Hamiltonian":
                raise qml.QuantumFunctionError(
                    "Adjoint differentiation method does not support Hamiltonian observables."
                )

            if not hasattr(m.obs, "base_name"):
                m.obs.base_name = None  # This is needed for when the observable is a tensor product

        if self.shots is not None:
            warnings.warn(
                "Requested adjoint differentiation to be computed with finite shots."
                " The derivative is always exact when using the adjoint differentiation method.",
                UserWarning,
            )

        # Initialization of state
        if starting_state is not None:
            ket = self._reshape(starting_state, [2] * self.num_wires)
        else:
            if not use_device_state:
                self.reset()
                self.execute(tape)
            ket = self._pre_rotated_state

        n_obs = len(tape.observables)
        bras = np.empty([n_obs] + [2] * self.num_wires, dtype=np.complex128)
        for kk in range(n_obs):
            bras[kk, ...] = self._apply_operation(ket, tape.observables[kk])

        expanded_ops = []
        for op in reversed(tape.operations):
            if op.num_params > 1:
                if isinstance(op, qml.Rot) and not op.inverse:
                    ops = op.decompose()
                    expanded_ops.extend(reversed(ops))
                else:
                    raise qml.QuantumFunctionError(
                        f"The {op.name} operation is not supported using "
                        'the "adjoint" differentiation method')
            else:
                if op.name not in ("QubitStateVector", "BasisState"):
                    expanded_ops.append(op)

        jac = np.zeros((len(tape.observables), len(tape.trainable_params)))

        param_number = len(tape._par_info) - 1  # pylint: disable=protected-access
        trainable_param_number = len(tape.trainable_params) - 1
        for op in expanded_ops:

            if (op.grad_method is not None) and (param_number
                                                 in tape.trainable_params):
                d_op_matrix = operation_derivative(op)

            op.inv()
            # Ideally use use op.adjoint() here
            # then we don't have to re-invert the operation at the end
            ket = self._apply_operation(ket, op)

            if op.grad_method is not None:
                if param_number in tape.trainable_params:

                    ket_temp = self._apply_unitary(ket, d_op_matrix, op.wires)

                    jac[:, trainable_param_number] = 2 * dot_product_real(
                        bras, ket_temp)

                    trainable_param_number -= 1
                param_number -= 1

            for kk in range(n_obs):
                bras[kk, ...] = self._apply_operation(bras[kk, ...], op)
            op.inv()

        return jac
    def adjoint_jacobian(self, tape):
        """Implements the adjoint method outlined in
        `Jones and Gacon <https://arxiv.org/abs/2009.02823>`__ to differentiate an input tape.

        After a forward pass, the circuit is reversed by iteratively applying inverse (adjoint)
        gates to scan backwards through the circuit. This method is similar to the reversible
        method, but has a lower time overhead and a similar memory overhead.

        .. note::
            The adjoint differentation method has the following restrictions:

            * As it requires knowledge of the statevector, only statevector simulator devices can be
              used.

            * Only expectation values are supported as measurements.

        Args:
            tape (.QuantumTape): circuit that the function takes the gradient of

        Returns:
            array: the derivative of the tape with respect to trainable parameters.
            Dimensions are ``(len(observables), len(trainable_params))``.

        Raises:
            QuantumFunctionError: if the input tape has measurements that are not expectation values
                or contains a multi-parameter operation aside from :class:`~.Rot`
        """

        for m in tape.measurements:
            if m.return_type is not qml.operation.Expectation:
                raise qml.QuantumFunctionError(
                    "Adjoint differentiation method does not support"
                    f" measurement {m.return_type.value}")

            if not hasattr(m.obs, "base_name"):
                m.obs.base_name = None  # This is needed for when the observable is a tensor product

        # Perform the forward pass.
        # Consider using caching and calling lower-level functionality. We just need the device to
        # be in the post-forward pass state.
        # https://github.com/PennyLaneAI/pennylane/pull/1032/files#r563441040
        self.reset()
        self.execute(tape)

        phi = self._reshape(self.state, [2] * self.num_wires)

        lambdas = [self._apply_operation(phi, obs) for obs in tape.observables]

        expanded_ops = []
        for op in reversed(tape.operations):
            if op.num_params > 1:
                if isinstance(op, qml.Rot) and not op.inverse:
                    ops = op.decomposition(*op.parameters, wires=op.wires)
                    expanded_ops.extend(reversed(ops))
                else:
                    raise QuantumFunctionError(
                        f"The {op.name} operation is not supported using "
                        'the "adjoint" differentiation method')
            else:
                if op.name not in ("QubitStateVector", "BasisState"):
                    expanded_ops.append(op)

        jac = np.zeros((len(tape.observables), len(tape.trainable_params)))
        dot_product_real = lambda a, b: self._real(qmlsum(self._conj(a) * b))

        param_number = len(tape._par_info) - 1  # pylint: disable=protected-access
        trainable_param_number = len(tape.trainable_params) - 1
        for op in expanded_ops:

            if (op.grad_method is not None) and (param_number
                                                 in tape.trainable_params):
                d_op_matrix = operation_derivative(op)

            op.inv()
            phi = self._apply_operation(phi, op)

            if op.grad_method is not None:
                if param_number in tape.trainable_params:
                    mu = self._apply_unitary(phi, d_op_matrix, op.wires)

                    jac_column = np.array([
                        2 * dot_product_real(lambda_, mu)
                        for lambda_ in lambdas
                    ])
                    jac[:, trainable_param_number] = jac_column
                    trainable_param_number -= 1
                param_number -= 1

            lambdas = [
                self._apply_operation(lambda_, op) for lambda_ in lambdas
            ]
            op.inv()

        return jac