def test_no_generator_raise(self): """Tests if the function raises a ValueError if the input operation has no generator""" op = qml.Rot(0.1, 0.2, 0.3, wires=0) with pytest.raises(ValueError, match="Operation Rot does not have a generator"): operation_derivative(op)
def test_multiparam_raise(self): """Test if the function raises a ValueError if the input operation is composed of multiple parameters""" class RotWithGen(qml.Rot): generator = [np.zeros((2, 2)), 1] op = RotWithGen(0.1, 0.2, 0.3, wires=0) with pytest.raises(ValueError, match="Operation RotWithGen is not written in terms of"): operation_derivative(op)
def test_phase(self): """Test if the function correctly returns the derivative of PhaseShift""" p = 0.3 op = qml.PhaseShift(p, wires=0) derivative = operation_derivative(op) expected_derivative = np.array([[0, 0], [0, 1j * np.exp(1j * p)]]) assert np.allclose(derivative, expected_derivative)
def test_rx(self): """Test if the function correctly returns the derivative of RX""" p = 0.3 op = qml.RX(p, wires=0) derivative = operation_derivative(op) expected_derivative = 0.5 * np.array( [[-np.sin(p / 2), -1j * np.cos(p / 2)], [-1j * np.cos(p / 2), -np.sin(p / 2)]] ) assert np.allclose(derivative, expected_derivative) op.inv() derivative_inv = operation_derivative(op) expected_derivative_inv = 0.5 * np.array( [[-np.sin(p / 2), 1j * np.cos(p / 2)], [1j * np.cos(p / 2), -np.sin(p / 2)]] ) assert not np.allclose(derivative, derivative_inv) assert np.allclose(derivative_inv, expected_derivative_inv)
def test_cry(self): """Test if the function correctly returns the derivative of CRY""" p = 0.3 op = qml.CRY(p, wires=[0, 1]) derivative = operation_derivative(op) expected_derivative = 0.5 * np.array([ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, -np.sin(p / 2), -np.cos(p / 2)], [0, 0, np.cos(p / 2), -np.sin(p / 2)], ]) assert np.allclose(derivative, expected_derivative)
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