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