From d42f5ee994afb9e873909f84ee8b9414129db940 Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Fri, 15 Mar 2024 05:11:03 -0400 Subject: [PATCH] Make `ApproxTimeEvolution` compatible with op math (#5362) [sc-58598] [sc-58407] **Context:** `ApproxTimeEvolution` tests are failing when used with new operator arithmetic **Description of the Change:** Update `ApproxTimeEvolution` to rely on the pauli rep. **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** --------- Co-authored-by: qottmann Co-authored-by: Mudit Pandey Co-authored-by: Korbinian Kottmann <43949391+Qottmann@users.noreply.github.com> Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Pietropaolo Frisoni --- doc/releases/changelog-dev.md | 3 + .../subroutines/approx_time_evolution.py | 55 ++++++------------- .../subroutines/commuting_evolution.py | 16 +++--- tests/circuit_graph/test_qasm.py | 6 +- .../test_approx_time_evolution.py | 19 +++---- .../test_commuting_evolution.py | 6 +- tests/test_qaoa.py | 18 +++--- 7 files changed, 53 insertions(+), 70 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 41b93c4ceff..d8e73a0bee4 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -105,6 +105,9 @@ * Upgraded `null.qubit` to the new device API. Also, added support for all measurements and various modes of differentiation. [(#5211)](https://github.com/PennyLaneAI/pennylane/pull/5211) +* `ApproxTimeEvolution` is now compatible with any operator that defines a `pauli_rep`. + [(#5362)](https://github.com/PennyLaneAI/pennylane/pull/5362) + * `Hamiltonian.pauli_rep` is now defined if the hamiltonian is a linear combination of paulis. [(#5377)](https://github.com/PennyLaneAI/pennylane/pull/5377) diff --git a/pennylane/templates/subroutines/approx_time_evolution.py b/pennylane/templates/subroutines/approx_time_evolution.py index 1c5f8bd0f9d..694821865c3 100644 --- a/pennylane/templates/subroutines/approx_time_evolution.py +++ b/pennylane/templates/subroutines/approx_time_evolution.py @@ -126,14 +126,13 @@ def _unflatten(cls, data, metadata): return cls(data[0], data[1], n=metadata[0]) def __init__(self, hamiltonian, time, n, id=None): - if not isinstance(hamiltonian, qml.Hamiltonian): + if getattr(hamiltonian, "pauli_rep", None) is None: raise ValueError( - f"hamiltonian must be of type pennylane.Hamiltonian, got {type(hamiltonian).__name__}" + f"hamiltonian must be a linear combination of pauli words, got {type(hamiltonian).__name__}" ) # extract the wires that the op acts on - wire_list = [term.wires for term in hamiltonian.ops] - wires = qml.wires.Wires.all_wires(wire_list) + wires = hamiltonian.wires self._hyperparameters = {"hamiltonian": hamiltonian, "n": n} @@ -187,38 +186,20 @@ def compute_decomposition( ... ) [PauliRot(0.1, ZZ, wires=[0, 1]), PauliRot(0.2, X, wires=[0]), PauliRot(0.3, X, wires=[1])] """ - pauli = {"Identity": "I", "PauliX": "X", "PauliY": "Y", "PauliZ": "Z"} - - theta = [] - pauli_words = [] - wires = [] - coeffs = coeffs_and_time[:-1] time = coeffs_and_time[-1] - for i, term in enumerate(hamiltonian.ops): - word = "" - - try: - if isinstance(term.name, str): - word = pauli[term.name] - - if isinstance(term.name, list): - word = "".join(pauli[j] for j in term.name) - - except KeyError as error: - raise ValueError( - f"hamiltonian must be written in terms of Pauli matrices, got {error}" - ) from error - - # skips terms composed solely of identities - if word.count("I") != len(word): - theta.append((2 * time * coeffs[i]) / n) - pauli_words.append(word) - wires.append(term.wires) - - op_list = [] - - for i in range(n): - for j, term in enumerate(pauli_words): - op_list.append(PauliRot(theta[j], term, wires=wires[j])) - return op_list + single_round = [] + with qml.QueuingManager.stop_recording(): + for pw, coeff in hamiltonian.pauli_rep.items(): + if len(pw) == 0: + continue + theta = 2 * time * coeff / n + term_str = "".join(pw.values()) + wires = qml.wires.Wires(pw.keys()) + single_round.append(PauliRot(theta, term_str, wires=wires)) + + full_decomp = single_round * n + if qml.QueuingManager.recording(): + _ = [qml.apply(op) for op in full_decomp] + + return full_decomp diff --git a/pennylane/templates/subroutines/commuting_evolution.py b/pennylane/templates/subroutines/commuting_evolution.py index 735ffe80e6f..e10252e912d 100644 --- a/pennylane/templates/subroutines/commuting_evolution.py +++ b/pennylane/templates/subroutines/commuting_evolution.py @@ -121,11 +121,12 @@ def __init__(self, hamiltonian, time, frequencies=None, shifts=None, id=None): generate_shift_rule, ) - if not isinstance(hamiltonian, qml.Hamiltonian): - type_name = type(hamiltonian).__name__ - raise TypeError(f"hamiltonian must be of type pennylane.Hamiltonian, got {type_name}") + if getattr(hamiltonian, "pauli_rep", None) is None: + raise TypeError( + f"hamiltonian must be a linear combination of pauli words. Got {hamiltonian}" + ) - trainable_hamiltonian = qml.math.requires_grad(hamiltonian.coeffs) + trainable_hamiltonian = qml.math.requires_grad(hamiltonian.data) if frequencies is not None and not trainable_hamiltonian: c, s = generate_shift_rule(frequencies, shifts).T recipe = qml.math.stack([c, qml.math.ones_like(c), s]).T @@ -142,14 +143,14 @@ def __init__(self, hamiltonian, time, frequencies=None, shifts=None, id=None): @staticmethod def compute_decomposition( - time, *coeffs, wires, hamiltonian, **kwargs + time, *_, wires, hamiltonian, **__ ): # pylint: disable=arguments-differ,unused-argument r"""Representation of the operator as a product of other operators. .. math:: O = O_1 O_2 \dots O_n. Args: - time_and_coeffs (list[tensor_like or float]): list of coefficients of the Hamiltonian, prepended by the time + *time_and_coeffs (list[tensor_like or float]): list of coefficients of the Hamiltonian, prepended by the time variable wires (Any or Iterable[Any]): wires that the operator acts on hamiltonian (.Hamiltonian): The commuting Hamiltonian defining the time-evolution operator. @@ -164,11 +165,10 @@ def compute_decomposition( list[.Operator]: decomposition of the operator """ # uses standard PauliRot decomposition through ApproxTimeEvolution. - hamiltonian = qml.Hamiltonian(coeffs, hamiltonian.ops) return [qml.ApproxTimeEvolution(hamiltonian, time, 1)] def adjoint(self): - hamiltonian = qml.Hamiltonian(self.parameters[1:], self.hyperparameters["hamiltonian"].ops) + hamiltonian = self.hyperparameters["hamiltonian"] time = self.parameters[0] frequencies = self.hyperparameters["frequencies"] shifts = self.hyperparameters["shifts"] diff --git a/tests/circuit_graph/test_qasm.py b/tests/circuit_graph/test_qasm.py index 9521b1c8bb9..c3f2beaf5f7 100644 --- a/tests/circuit_graph/test_qasm.py +++ b/tests/circuit_graph/test_qasm.py @@ -117,9 +117,9 @@ def test_to_ApproxTimeEvolution(self): include "qelib1.inc"; qreg q[2]; creg c[2]; - cx q[1],q[0]; - rz(2.0) q[0]; - cx q[1],q[0]; + cx q[0],q[1]; + rz(2.0) q[1]; + cx q[0],q[1]; measure q[0] -> c[0]; measure q[1] -> c[1]; """ diff --git a/tests/templates/test_subroutines/test_approx_time_evolution.py b/tests/templates/test_subroutines/test_approx_time_evolution.py index 42587c537a3..044f261ee1b 100644 --- a/tests/templates/test_subroutines/test_approx_time_evolution.py +++ b/tests/templates/test_subroutines/test_approx_time_evolution.py @@ -70,9 +70,9 @@ class TestDecomposition: 2, [ qml.PauliRot(4.0, "X", wires=["a"]), - qml.PauliRot(1.0, "ZX", wires=["b", "a"]), + qml.PauliRot(1.0, "XZ", wires=["a", "b"]), qml.PauliRot(4.0, "X", wires=["a"]), - qml.PauliRot(1.0, "ZX", wires=["b", "a"]), + qml.PauliRot(1.0, "XZ", wires=["a", "b"]), ], ), ( @@ -94,8 +94,8 @@ class TestDecomposition: 1, [ qml.PauliRot(8.0, "X", wires=["a"]), - qml.PauliRot(2.0, "ZX", wires=[-15, "a"]), - qml.PauliRot(2.0, "IY", wires=[0, -15]), + qml.PauliRot(2.0, "XZ", wires=["a", -15]), + qml.PauliRot(2.0, "Y", wires=[-15]), ], ), ], @@ -107,10 +107,7 @@ def test_evolution_operations(self, time, hamiltonian, steps, expected_queue): queue = op.expand().operations for expected_gate, gate in zip(expected_queue, queue): - prep = [gate.parameters, gate.wires] - target = [expected_gate.parameters, expected_gate.wires] - - assert prep == target + assert qml.equal(expected_gate, gate) @pytest.mark.parametrize( ("time", "hamiltonian", "steps", "expectation"), @@ -203,7 +200,9 @@ def circuit(): qml.ApproxTimeEvolution(hamiltonian, 2, 3) return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_wires)] - with pytest.raises(ValueError, match="hamiltonian must be of type pennylane.Hamiltonian"): + with pytest.raises( + ValueError, match="hamiltonian must be a linear combination of pauli words" + ): circuit() @pytest.mark.parametrize( @@ -228,7 +227,7 @@ def circuit(): return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_wires)] with pytest.raises( - ValueError, match="hamiltonian must be written in terms of Pauli matrices" + ValueError, match="hamiltonian must be a linear combination of pauli words" ): circuit() diff --git a/tests/templates/test_subroutines/test_commuting_evolution.py b/tests/templates/test_subroutines/test_commuting_evolution.py index 137ed740c3c..60124cf552a 100644 --- a/tests/templates/test_subroutines/test_commuting_evolution.py +++ b/tests/templates/test_subroutines/test_commuting_evolution.py @@ -96,7 +96,7 @@ def test_decomposition_expand(): decomp = op.decomposition()[0] assert isinstance(decomp, qml.ApproxTimeEvolution) - assert all(decomp.hyperparameters["hamiltonian"].coeffs == hamiltonian.coeffs) + assert qml.math.allclose(decomp.hyperparameters["hamiltonian"].data, hamiltonian.data) assert decomp.hyperparameters["n"] == 1 tape = op.expand() @@ -141,9 +141,9 @@ class TestInputs: """Tests for input validation of `CommutingEvolution`.""" def test_invalid_hamiltonian(self): - """Tests TypeError is raised if `hamiltonian` is not type `qml.Hamiltonian`.""" + """Tests TypeError is raised if `hamiltonian` does not have a pauli rep.""" - invalid_operator = qml.PauliX(0) + invalid_operator = qml.Hermitian(np.eye(2), 0) assert pytest.raises(TypeError, qml.CommutingEvolution, invalid_operator, 1) diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index 2854d79ed30..3f511e8a5c6 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -1110,12 +1110,12 @@ def test_cost_layer_errors(self): [ qaoa.xy_mixer(Graph([(0, 1), (1, 2), (2, 0)])), [ - qml.PauliRot(1, "XX", wires=[0, 1]), - qml.PauliRot(1, "YY", wires=[0, 1]), - qml.PauliRot(1, "XX", wires=[0, 2]), - qml.PauliRot(1, "YY", wires=[0, 2]), - qml.PauliRot(1, "XX", wires=[1, 2]), - qml.PauliRot(1, "YY", wires=[1, 2]), + qml.PauliRot(1, "XX", wires=[1, 0]), + qml.PauliRot(1, "YY", wires=[1, 0]), + qml.PauliRot(1, "XX", wires=[2, 0]), + qml.PauliRot(1, "YY", wires=[2, 0]), + qml.PauliRot(1, "XX", wires=[2, 1]), + qml.PauliRot(1, "YY", wires=[2, 1]), ], ], ], @@ -1146,9 +1146,9 @@ def test_mixer_layer_output(self, mixer, gates): [ qaoa.maxcut(Graph([(0, 1), (1, 2), (2, 0)]))[0], [ - qml.PauliRot(1, "ZZ", wires=[0, 1]), - qml.PauliRot(1, "ZZ", wires=[0, 2]), - qml.PauliRot(1, "ZZ", wires=[1, 2]), + qml.PauliRot(1, "ZZ", wires=[1, 0]), + qml.PauliRot(1, "ZZ", wires=[2, 0]), + qml.PauliRot(1, "ZZ", wires=[2, 1]), ], ], ],