diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py index 83fa90a9275..bf9b0f8a69e 100644 --- a/pennylane/ops/functions/equal.py +++ b/pennylane/ops/functions/equal.py @@ -324,11 +324,9 @@ def _equal_operators( atol=1e-9, ): """Default function to determine whether two Operator objects are equal.""" - print("here") if not isinstance( op2, type(op1) ): # clarifies cases involving PauliX/Y/Z (Observable/Operation) - print("oh no") return False if isinstance(op1, qml.Identity): @@ -338,26 +336,21 @@ def _equal_operators( return True if op1.arithmetic_depth != op2.arithmetic_depth: - print("oh no2") return False if op1.arithmetic_depth > 0: # Other dispatches cover cases of operations with arithmetic depth > 0. # If any new operations are added with arithmetic depth > 0, a new dispatch # should be created for them. - print("oh no3") return False if not all( qml.math.allclose(d1, d2, rtol=rtol, atol=atol) for d1, d2 in zip(op1.data, op2.data) ): - print("oh no4") return False if op1.wires != op2.wires: - print("oh no5") return False if op1.hyperparameters != op2.hyperparameters: - print("oh no6") return False if check_trainability: @@ -442,7 +435,7 @@ def _equal_adjoint(op1: Adjoint, op2: Adjoint, **kwargs): # pylint: disable=unused-argument def _equal_scalar_symbolic_op(op1: ScalarSymbolicOp, op2: ScalarSymbolicOp, **kwargs): """Determine whether two Exp objects are equal""" - + if not qml.math.allclose(op1.scalar, op2.scalar, rtol=kwargs["rtol"], atol=kwargs["atol"]): return False if kwargs["check_trainability"]: @@ -453,18 +446,15 @@ def _equal_scalar_symbolic_op(op1: ScalarSymbolicOp, op2: ScalarSymbolicOp, **kw return False return qml.equal(op1.base, op2.base, **kwargs) + @_equal.register # pylint: disable=unused-argument def _equal_sprod(op1: SProd, op2: SProd, **kwargs): """Determine whether two SProd objects are equal""" - rtol, atol = (kwargs["rtol"], kwargs["atol"]) - if op1.pauli_rep is not None and (op1.pauli_rep == op2.pauli_rep): # shortcut check return True - if not qml.math.allclose(op1.scalar, op2.scalar, rtol=rtol, atol=atol): - return False - return qml.equal(op1.base, op2.base) + return _equal_scalar_symbolic_op(op1, op2, **kwargs) @_equal.register diff --git a/pennylane/templates/subroutines/commuting_evolution.py b/pennylane/templates/subroutines/commuting_evolution.py index ca59e588a4f..70c85afa203 100644 --- a/pennylane/templates/subroutines/commuting_evolution.py +++ b/pennylane/templates/subroutines/commuting_evolution.py @@ -16,8 +16,9 @@ """ # pylint: disable-msg=too-many-arguments,import-outside-toplevel import pennylane as qml -from pennylane.operation import AnyWires, ParameterFrequenciesUndefinedError +from pennylane.operation import AnyWires, ParameterFrequenciesUndefinedError, MatrixUndefinedError from pennylane.ops.op_math import ScalarSymbolicOp +from pennylane.typing import TensorLike class CommutingEvolution(ScalarSymbolicOp): @@ -131,6 +132,8 @@ def __init__(self, hamiltonian, time, frequencies=None, shifts=None, id=None): recipe = qml.math.stack([c, qml.math.ones_like(c), s]).T self.grad_recipe = (recipe,) + (None,) * len(hamiltonian.data) self.grad_method = "A" + else: + self.grad_recipe = (None,) * (1 + len(hamiltonian.data)) super().__init__(hamiltonian, scalar=time, id=id) self.hyperparameters["frequencies"] = frequencies @@ -142,17 +145,26 @@ def parameter_frequencies(self): # Note that because of the coefficients of the Hamiltonian, we do not have # parameter_frequencies even if "frequencies" are provided at initialization! raise ParameterFrequenciesUndefinedError( - "CommutingEvolution only has no parameter frequencies defined." + "CommutingEvolution has no parameter frequencies defined." ) @property def _queue_category(self): return "_ops" + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_matrix(self): + return False + @staticmethod def _matrix(scalar, mat): return qml.math.expm(-1j * scalar * mat) + def matrix(self, wire_order=None) -> TensorLike: + """Raise a MatrixUndefinedError for now to force decomposition on DefaultQubit.""" + raise MatrixUndefinedError("CommutingEvolution does not define a matrix itself.") + # pylint: disable=invalid-overridden-method, arguments-renamed @property def has_decomposition(self): @@ -170,7 +182,6 @@ def decomposition(self): def adjoint(self): frequencies = self.hyperparameters["frequencies"] shifts = self.hyperparameters["shifts"] - return CommutingEvolution(self.base, -self.scalar, frequencies, shifts) # pylint: disable=arguments-renamed,invalid-overridden-method @@ -184,7 +195,7 @@ def diagonalizing_gates(self): # pylint: disable=arguments-renamed, invalid-overridden-method @property def has_generator(self): - return self.base.is_hermitian and not np.real(self.coeff) + return True def generator(self): r"""Generator of an operator that is in single-parameter-form. diff --git a/tests/templates/test_subroutines/test_commuting_evolution.py b/tests/templates/test_subroutines/test_commuting_evolution.py index a774a00ee74..9084ba12d0c 100644 --- a/tests/templates/test_subroutines/test_commuting_evolution.py +++ b/tests/templates/test_subroutines/test_commuting_evolution.py @@ -22,113 +22,163 @@ from pennylane import numpy as np import copy +from pennylane.operation import ParameterFrequenciesUndefinedError -def test_standard_validity(): - """Run standard tests of operation validity.""" - H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1) - time = 0.5 - frequencies = (2, 4) - shifts = (1, 0.5) - op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts) - qml.ops.functions.assert_valid(op) +class TestBasicProperties: + """Test basic methods/attributes/properties of CommutingEvolution.""" -# pylint: disable=protected-access -def test_flatten_unflatten(): - """Unit tests for the flatten and unflatten methods.""" - H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1) - time = 0.5 - frequencies = (2, 4) - shifts = (1, 0.5) - op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts) - data, metadata = op._flatten() + def test_standard_validity(self): + """Run standard tests of operation validity.""" + H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1) + time = 0.5 + frequencies = (2, 4) + shifts = (1, 0.5) + op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts) + qml.ops.functions.assert_valid(op) + + # pylint: disable=protected-access + def test_flatten_unflatten(self): + """Unit tests for the flatten and unflatten methods.""" + H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1) + time = 0.5 + frequencies = (2, 4) + shifts = (1, 0.5) + op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts) + data, metadata = op._flatten() + + assert hash(metadata) + + assert len(data) == 2 + assert data[1] is H + assert data[0] == time + assert metadata == (frequencies, shifts) - assert hash(metadata) + new_op = type(op)._unflatten(*op._flatten()) + assert qml.equal(op, new_op) + assert op is not new_op - assert len(data) == 2 - assert data[1] is H - assert data[0] == time - assert metadata == (frequencies, shifts) + def test_adjoint(self): + """Tests the CommutingEvolution.adjoint method provides the correct adjoint operation.""" + + n_wires = 2 + dev = qml.device("default.qubit", wires=n_wires) - new_op = type(op)._unflatten(*op._flatten()) - assert qml.equal(op, new_op) - assert op is not new_op + obs = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(0) @ qml.PauliX(1)] + coeffs = [1, -1] + hamiltonian = qml.Hamiltonian(coeffs, obs) + frequencies = (2,) + @qml.qnode(dev) + def adjoint_evolution_circuit(time): + for i in range(n_wires): + qml.Hadamard(i) + qml.adjoint(qml.CommutingEvolution)(hamiltonian, time, frequencies) + return qml.expval(qml.PauliZ(1)), qml.state() -def test_adjoint(): - """Tests the CommutingEvolution.adjoint method provides the correct adjoint operation.""" + @qml.qnode(dev) + def evolution_circuit(time): + for i in range(n_wires): + qml.Hadamard(i) + qml.CommutingEvolution(hamiltonian, time, frequencies) + return qml.expval(qml.PauliZ(1)), qml.state() - n_wires = 2 - dev = qml.device("default.qubit", wires=n_wires) + res1, state1 = evolution_circuit(0.13) + res2, state2 = adjoint_evolution_circuit(-0.13) - obs = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(0) @ qml.PauliX(1)] - coeffs = [1, -1] - hamiltonian = qml.Hamiltonian(coeffs, obs) - frequencies = (2,) + assert res1 == res2 + assert all(np.isclose(state1, state2)) - @qml.qnode(dev) - def adjoint_evolution_circuit(time): - for i in range(n_wires): - qml.Hadamard(i) - qml.adjoint(qml.CommutingEvolution)(hamiltonian, time, frequencies) - return qml.expval(qml.PauliZ(1)), qml.state() + def test_queuing(self): + """Test that CommutingEvolution de-queues the input hamiltonian.""" - @qml.qnode(dev) - def evolution_circuit(time): - for i in range(n_wires): - qml.Hadamard(i) - qml.CommutingEvolution(hamiltonian, time, frequencies) - return qml.expval(qml.PauliZ(1)), qml.state() + with qml.queuing.AnnotatedQueue() as q: + H = qml.X(0) + qml.Y(1) + op = qml.CommutingEvolution(H, 0.1, (2,)) - res1, state1 = evolution_circuit(0.13) - res2, state2 = adjoint_evolution_circuit(-0.13) + assert len(q.queue) == 1 + assert q.queue[0] is op + assert op._queue_category == "_ops" - assert res1 == res2 - assert all(np.isclose(state1, state2)) + def test_decomposition_expand(self): + """Test that the decomposition of CommutingEvolution is an ApproxTimeEvolution with one step.""" + hamiltonian = 0.5 * qml.PauliX(0) @ qml.PauliY(1) + time = 2.345 -def test_queuing(): - """Test that CommutingEvolution de-queues the input hamiltonian.""" + op = qml.CommutingEvolution(hamiltonian, time) - with qml.queuing.AnnotatedQueue() as q: - H = qml.X(0) + qml.Y(1) - op = qml.CommutingEvolution(H, 0.1, (2,)) + assert op.has_decomposition is True - assert len(q.queue) == 1 - assert q.queue[0] is op + decomp = op.decomposition()[0] + assert isinstance(decomp, qml.ApproxTimeEvolution) + assert qml.math.allclose(decomp.hyperparameters["hamiltonian"].data, hamiltonian.data) + assert decomp.hyperparameters["n"] == 1 -def test_decomposition_expand(): - """Test that the decomposition of CommutingEvolution is an ApproxTimeEvolution with one step.""" + tape = op.expand() + assert len(tape) == 1 + assert isinstance(tape[0], qml.ApproxTimeEvolution) - hamiltonian = 0.5 * qml.PauliX(0) @ qml.PauliY(1) - time = 2.345 + def test_matrix(self): + """Test that the matrix of commuting evolution is the same as exponentiating -1j * t the hamiltonian.""" - op = qml.CommutingEvolution(hamiltonian, time) + h = 2.34 * qml.PauliX(0) + time = 0.234 + op = qml.CommutingEvolution(h, time) - decomp = op.decomposition()[0] + assert op.has_matrix is False - assert isinstance(decomp, qml.ApproxTimeEvolution) - assert qml.math.allclose(decomp.hyperparameters["hamiltonian"].data, hamiltonian.data) - assert decomp.hyperparameters["n"] == 1 + mat = qml.matrix(op) + mat2 = op._matrix(time, qml.matrix(h)) - tape = op.expand() - assert len(tape) == 1 - assert isinstance(tape[0], qml.ApproxTimeEvolution) + expected = expm(-1j * time * qml.matrix(h)) + assert qml.math.allclose(mat, expected) + assert qml.math.allclose(mat2, expected) -def test_matrix(): - """Test that the matrix of commuting evolution is the same as exponentiating -1j * t the hamiltonian.""" + @pytest.mark.parametrize("frequencies", [None, (2, 4)]) + def test_parameter_frequencies(self, frequencies): + """Test that no parameter-frequencies are defined.""" + h = 2.34 * qml.PauliX(0) + time = 0.234 + op = qml.CommutingEvolution(h, time, frequencies=frequencies) + with pytest.raises(ParameterFrequenciesUndefinedError, match="CommutingEvolution has no"): + _ = op.parameter_frequencies - h = 2.34 * qml.PauliX(0) - time = 0.234 - op = qml.CommutingEvolution(h, time) + @pytest.mark.parametrize("frequencies", [None, (1,)]) + def test_grad_method_grad_recipe(self, frequencies): + """Test that no parameter-frequencies are defined.""" + h = 2.34 * qml.PauliX(0) + time = 0.234 + op = qml.CommutingEvolution(h, time, frequencies=frequencies) - mat = qml.matrix(op) + if frequencies is None: + assert op.grad_method is None + assert op.grad_recipe == (None,) * 2 + else: + assert op.grad_method == "A" + assert len(op.grad_recipe) == 2 + assert qml.math.allclose( + op.grad_recipe[0], [[0.5, 1.0, np.pi / 2], [-0.5, 1.0, -np.pi / 2]] + ) + assert op.grad_recipe[1] is None - expected = expm(-1j * time * qml.matrix(h)) + def test_diagonalizing_gates(self): + """Test the diagonalizing gates of CommutingEvolution.""" + h = 2.34 * qml.PauliX(0) + time = 0.234 + op = qml.CommutingEvolution(h, time) + assert op.has_diagonalizing_gates is True + assert op.diagonalizing_gates() == qml.PauliX(0).diagonalizing_gates() - assert qml.math.allclose(mat, expected) + def test_generator(self): + """Test the generator of CommutingEvolution.""" + h = 2.34 * qml.PauliX(0) + time = 0.234 + op = qml.CommutingEvolution(h, time) + assert op.has_generator is True + assert op.generator() == -h def test_forward_execution(): @@ -236,7 +286,6 @@ def circuit(time, coeffs): x_vals = [np.array(x, requires_grad=True) for x in np.linspace(-np.pi, np.pi, num=10)] circuit(x_vals[0], diff_coeffs) - print(circuit.tape[1].grad_recipe) grads_finite_diff = [ np.hstack(qml.gradients.finite_diff(circuit)(x, diff_coeffs)) for x in x_vals