From eb39402bf52c6f45338465820c7acd557af0d7b7 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 18 Jun 2024 15:12:50 -0400 Subject: [PATCH] Clean up `Device.batch_transform` to use `split_non_commuting` (#5828) **Context:** https://github.com/PennyLaneAI/pennylane/pull/5729 introduced the unified `split_non_commuting`. Now the legacy device can use `split_non_commuting` in all scenarios. **Description of the Change:** Cleans up `batch_transform` to use `split_non_commuting` **Benefits:** Cleaner code, prepares for the deprecation of `hamiltonian_expand` and `sum_expand`. **Related GitHub Issues:** [sc-61253] --- doc/releases/changelog-dev.md | 1 + pennylane/_device.py | 113 +++++++++------- pennylane/devices/qubit/sampling.py | 25 +++- tests/conftest.py | 6 + .../test_default_qubit_tracking.py | 36 ++--- .../test_qutrit_mixed_tracking.py | 2 +- tests/interfaces/test_autograd.py | 50 ++----- tests/interfaces/test_autograd_qnode.py | 2 +- tests/interfaces/test_jax_jit_qnode.py | 4 +- tests/interfaces/test_jax_qnode.py | 4 +- tests/interfaces/test_tensorflow_qnode.py | 2 +- tests/interfaces/test_torch_qnode.py | 2 +- tests/test_device.py | 12 +- tests/test_qnode_legacy.py | 2 +- tests/test_vqe.py | 127 +++++++----------- 15 files changed, 175 insertions(+), 213 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index f561a58642f..a6be1e06fbe 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -115,6 +115,7 @@ * `qml.transforms.split_non_commuting` can now handle circuits containing measurements of multi-term observables. [(#5729)](https://github.com/PennyLaneAI/pennylane/pull/5729) [(#5853)](https://github.com/PennyLaneAI/pennylane/pull/5838) + [(#5828)](https://github.com/PennyLaneAI/pennylane/pull/5828) [(#5869)](https://github.com/PennyLaneAI/pennylane/pull/5869) * The qchem module has dedicated functions for calling `pyscf` and `openfermion` backends. The diff --git a/pennylane/_device.py b/pennylane/_device.py index b089523f8fb..53c95cee816 100644 --- a/pennylane/_device.py +++ b/pennylane/_device.py @@ -26,15 +26,11 @@ import pennylane as qml from pennylane.measurements import ( - CountsMP, Expectation, - ExpectationMP, MeasurementProcess, MidMeasureMP, Probability, - ProbabilityMP, Sample, - SampleMP, ShadowExpvalMP, State, Variance, @@ -738,79 +734,92 @@ def batch_transform(self, circuit: QuantumTape): the sequence of circuits to be executed, and a post-processing function to be applied to the list of evaluated circuit results. """ - supports_hamiltonian = self.supports_observable("Hamiltonian") - supports_sum = self.supports_observable("Sum") + + def null_postprocess(results): + return results[0] + finite_shots = self.shots is not None - grouping_known = all( - obs.grouping_indices is not None - for obs in circuit.observables - if isinstance(obs, (Hamiltonian, LinearCombination)) + has_shadow = any(isinstance(m, ShadowExpvalMP) for m in circuit.measurements) + is_analytic_or_shadow = not finite_shots or has_shadow + all_obs_usable = self._all_multi_term_obs_supported(circuit) + exists_multi_term_obs = any( + isinstance(m.obs, (Hamiltonian, Sum, Prod, SProd)) for m in circuit.measurements ) - # device property present in braket plugin - use_grouping = getattr(self, "use_grouping", True) - - hamiltonian_in_obs = any( - isinstance(obs, (Hamiltonian, LinearCombination)) for obs in circuit.observables + has_overlapping_wires = len(circuit.obs_sharing_wires) > 0 + single_hamiltonian = len(circuit.measurements) == 1 and isinstance( + circuit.measurements[0].obs, (Hamiltonian, Sum) ) - - expval_sum_or_prod_in_obs = any( - isinstance(m.obs, (Sum, Prod, SProd)) and isinstance(m, ExpectationMP) - for m in circuit.measurements + single_hamiltonian_with_grouping_known = ( + single_hamiltonian and circuit.measurements[0].obs.grouping_indices is not None ) - is_shadow = any(isinstance(m, ShadowExpvalMP) for m in circuit.measurements) + if not getattr(self, "use_grouping", True) and single_hamiltonian and all_obs_usable: + # Special logic for the braket plugin + circuits = [circuit] + processing_fn = null_postprocess - hamiltonian_unusable = not supports_hamiltonian or (finite_shots and not is_shadow) + elif not exists_multi_term_obs and not has_overlapping_wires: + circuits = [circuit] + processing_fn = null_postprocess - if hamiltonian_in_obs and (hamiltonian_unusable or (use_grouping and grouping_known)): - # If the observable contains a Hamiltonian and the device does not - # support Hamiltonians, or if the simulation uses finite shots, or - # if the Hamiltonian explicitly specifies an observable grouping, - # split tape into multiple tapes of diagonalizable known observables. - try: - circuits, hamiltonian_fn = qml.transforms.hamiltonian_expand(circuit, group=False) - except ValueError: - circuits, hamiltonian_fn = qml.transforms.sum_expand(circuit) + elif is_analytic_or_shadow and all_obs_usable and not has_overlapping_wires: + circuits = [circuit] + processing_fn = null_postprocess - elif expval_sum_or_prod_in_obs and not is_shadow and not supports_sum: - circuits, hamiltonian_fn = qml.transforms.sum_expand(circuit) + elif single_hamiltonian_with_grouping_known: - elif ( - len(circuit.obs_sharing_wires) > 0 - and not hamiltonian_in_obs - and all( - not isinstance(m, (SampleMP, ProbabilityMP, CountsMP)) for m in circuit.measurements - ) - ): - # Check for case of non-commuting terms and that there are no Hamiltonians - # TODO: allow for Hamiltonians in list of observables as well. - circuits, hamiltonian_fn = qml.transforms.split_non_commuting(circuit) + # Use qwc grouping if the circuit contains a single measurement of a + # Hamiltonian/Sum with grouping indices already calculated. + circuits, processing_fn = qml.transforms.split_non_commuting(circuit, "qwc") - else: - # otherwise, return the output of an identity transform - circuits = [circuit] + elif any(isinstance(m.obs, (Hamiltonian, LinearCombination)) for m in circuit.measurements): - def hamiltonian_fn(res): - return res[0] + # Otherwise, use wire-based grouping if the circuit contains a Hamiltonian + # that is potentially very large. + circuits, processing_fn = qml.transforms.split_non_commuting(circuit, "wires") - # Check whether the circuit was broadcasted (then the Hamiltonian-expanded - # ones will be as well) and whether broadcasting is supported + else: + circuits, processing_fn = qml.transforms.split_non_commuting(circuit) + + # Check whether the circuit was broadcasted and whether broadcasting is supported if circuit.batch_size is None or self.capabilities().get("supports_broadcasting"): # If the circuit wasn't broadcasted or broadcasting is supported, no action required - return circuits, hamiltonian_fn + return circuits, processing_fn # Expand each of the broadcasted Hamiltonian-expanded circuits expanded_tapes, expanded_fn = qml.transforms.broadcast_expand(circuits) # Chain the postprocessing functions of the broadcasted-tape expansions and the Hamiltonian # expansion. Note that the application order is reversed compared to the expansion order, - # i.e. while we first applied `hamiltonian_expand` to the tape, we need to process the + # i.e. while we first applied `split_non_commuting` to the tape, we need to process the # results from the broadcast expansion first. def total_processing(results): - return hamiltonian_fn(expanded_fn(results)) + return processing_fn(expanded_fn(results)) return expanded_tapes, total_processing + def _all_multi_term_obs_supported(self, circuit): + """Check whether all multi-term observables in the circuit are supported.""" + + for mp in circuit.measurements: + + if mp.obs is None: + # Some measurements are not observable based. + continue + + if mp.obs.name == "LinearCombination" and not self.supports_observable("Hamiltonian"): + return False + + if mp.obs.name in ( + "Hamiltonian", + "Sum", + "Prod", + "SProd", + ) and not self.supports_observable(mp.obs.name): + return False + + return True + @property def op_queue(self): """The operation queue to be applied. diff --git a/pennylane/devices/qubit/sampling.py b/pennylane/devices/qubit/sampling.py index 0c06125cc45..2a585655c2e 100644 --- a/pennylane/devices/qubit/sampling.py +++ b/pennylane/devices/qubit/sampling.py @@ -103,7 +103,30 @@ def _get_num_executions_for_expval_H(obs): indices = obs.grouping_indices if indices: return len(indices) - return sum(int(not isinstance(o, qml.Identity)) for o in obs.terms()[1]) + return _get_num_wire_groups_for_expval_H(obs) + + +def _get_num_wire_groups_for_expval_H(obs): + _, obs_list = obs.terms() + wires_list = [] + added_obs = [] + num_groups = 0 + for o in obs_list: + if o in added_obs: + continue + if isinstance(o, qml.Identity): + continue + added = False + for wires in wires_list: + if len(qml.wires.Wires.shared_wires([wires, o.wires])) == 0: + added_obs.append(o) + added = True + break + if not added: + added_obs.append(o) + wires_list.append(o.wires) + num_groups += 1 + return num_groups def _get_num_executions_for_sum(obs): diff --git a/tests/conftest.py b/tests/conftest.py index 7f2eb29e46f..4c8eba9dc6d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -228,6 +228,12 @@ def new_opmath_only(): pytest.skip("This feature only works with new opmath enabled") +@pytest.fixture +def legacy_opmath_only(): + if qml.operation.active_new_opmath(): + pytest.skip("This test exclusively tests legacy opmath") + + ####################################################################### try: diff --git a/tests/devices/default_qubit/test_default_qubit_tracking.py b/tests/devices/default_qubit/test_default_qubit_tracking.py index 0fe692ed864..116a2fe3663 100644 --- a/tests/devices/default_qubit/test_default_qubit_tracking.py +++ b/tests/devices/default_qubit/test_default_qubit_tracking.py @@ -195,40 +195,28 @@ def circuit_3(y): shot_testing_combos = [ # expval combinations - ([qml.expval(qml.PauliX(0))], 1, 10), - ([qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0))], 2, 20), + ([qml.expval(qml.X(0))], 1, 10), + ([qml.expval(qml.X(0)), qml.expval(qml.Y(0))], 2, 20), # Hamiltonian test cases - ([qml.expval(qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliX(1)]))], 2, 20), - ( - [qml.expval(qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliX(1)], grouping_type="qwc"))], - 1, - 10, - ), - ( - [qml.expval(qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliY(0)], grouping_type="qwc"))], - 2, - 20, - ), + ([qml.expval(qml.Hamiltonian([1, 0.5, 1], [qml.X(0), qml.Y(0), qml.X(1)]))], 2, 20), + ([qml.expval(qml.Hamiltonian([1, 1], [qml.X(0), qml.X(1)], grouping_type="qwc"))], 1, 10), + ([qml.expval(qml.Hamiltonian([1, 1], [qml.X(0), qml.Y(0)], grouping_type="qwc"))], 2, 20), # op arithmetic test cases - ([qml.expval(qml.sum(qml.PauliX(0), qml.PauliY(0)))], 2, 20), - ([qml.expval(qml.sum(qml.PauliX(0), qml.PauliX(0) @ qml.PauliX(1)))], 1, 10), - ([qml.expval(qml.sum(qml.PauliX(0), qml.Hadamard(0)))], 2, 20), - ( - [qml.expval(qml.sum(qml.PauliX(0), qml.PauliY(1) @ qml.PauliX(1), grouping_type="qwc"))], - 1, - 10, - ), + ([qml.expval(qml.sum(qml.X(0), qml.Y(0)))], 2, 20), + ([qml.expval(qml.sum(qml.X(0), qml.X(0) @ qml.X(1)))], 1, 10), + ([qml.expval(qml.sum(qml.X(0), qml.Hadamard(0)))], 2, 20), + ([qml.expval(qml.sum(qml.X(0), qml.Y(1) @ qml.X(1), grouping_type="qwc"))], 1, 10), ( [ - qml.expval(qml.prod(qml.PauliX(0), qml.PauliX(1))), - qml.expval(qml.prod(qml.PauliX(1), qml.PauliX(2))), + qml.expval(qml.prod(qml.X(0), qml.X(1))), + qml.expval(qml.prod(qml.X(1), qml.X(2))), ], 1, 10, ), # computational basis measurements ([qml.probs(wires=(0, 1)), qml.sample(wires=(0, 1))], 1, 10), - ([qml.probs(wires=(0, 1)), qml.sample(wires=(0, 1)), qml.expval(qml.PauliX(0))], 2, 20), + ([qml.probs(wires=(0, 1)), qml.sample(wires=(0, 1)), qml.expval(qml.X(0))], 2, 20), # classical shadows ([qml.shadow_expval(H0)], 10, 10), ([qml.shadow_expval(H0), qml.probs(wires=(0, 1))], 11, 20), diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py index a735157784f..41a324aab7b 100644 --- a/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py +++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py @@ -157,7 +157,7 @@ def circuit_3(y): ([qml.expval(qml.GellMann(0, 1))], 1, 10), ([qml.expval(qml.GellMann(0, 1)), qml.expval(qml.GellMann(0, 2))], 2, 20), # Hamiltonian test cases - ([qml.expval(qml.Hamiltonian([1, 1], [qml.GellMann(0, 1), qml.GellMann(1, 5)]))], 2, 20), + ([qml.expval(qml.Hamiltonian([1, 1], [qml.GellMann(0, 1), qml.GellMann(1, 5)]))], 1, 10), # op arithmetic test cases ([qml.expval(qml.sum(qml.GellMann(0, 1), qml.GellMann(1, 4)))], 2, 20), ( diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py index 916f5d5bc51..79bffccf255 100644 --- a/tests/interfaces/test_autograd.py +++ b/tests/interfaces/test_autograd.py @@ -182,7 +182,6 @@ class TestBatchTransformExecution: """Tests to ensure batch transforms can be correctly executed via qml.execute and map_batch_transform""" - @pytest.mark.usefixtures("use_new_opmath") def test_no_batch_transform(self, mocker): """Test that batch transforms can be disabled and enabled""" dev = qml.device("default.qubit.legacy", wires=2, shots=100000) @@ -200,47 +199,20 @@ def test_no_batch_transform(self, mocker): tape = qml.tape.QuantumScript.from_queue(q) spy = mocker.spy(dev, "batch_transform") - res = qml.execute([tape], dev, None, device_batch_transform=False) - assert np.allclose(res[0], np.cos(y), atol=0.1) - - spy.assert_not_called() - - res = qml.execute([tape], dev, None, device_batch_transform=True) - spy.assert_called() - - assert isinstance(res[0], np.ndarray) - assert res[0].shape == () - assert np.allclose(res[0], np.cos(y), atol=0.1) - - @pytest.mark.usefixtures("use_legacy_opmath") - def test_no_batch_transform_legacy_opmath(self, mocker): - """Test functionality to enable and disable""" - dev = qml.device("default.qubit.legacy", wires=2, shots=100000) - - H = qml.PauliZ(0) @ qml.PauliZ(1) - qml.PauliX(0) - x = 0.6 - y = 0.2 - - with qml.queuing.AnnotatedQueue() as q: - qml.RX(x, wires=0) - qml.RY(y, wires=1) - qml.CNOT(wires=[0, 1]) - qml.expval(H) - - tape = qml.tape.QuantumScript.from_queue(q) - spy = mocker.spy(dev, "batch_transform") - - with pytest.raises(AssertionError, match="Hamiltonian must be used with shots=None"): + if not qml.operation.active_new_opmath(): + with pytest.raises(AssertionError, match="Hamiltonian must be used with shots=None"): + _ = qml.execute([tape], dev, None, device_batch_transform=False) + else: res = qml.execute([tape], dev, None, device_batch_transform=False) + assert np.allclose(res[0], np.cos(y), atol=0.1) spy.assert_not_called() res = qml.execute([tape], dev, None, device_batch_transform=True) spy.assert_called() - assert isinstance(res[0], np.ndarray) - assert res[0].shape == () - assert np.allclose(res[0], np.cos(y), atol=0.1) + assert qml.math.shape(res[0]) == () + assert np.allclose(res[0], np.cos(y), rtol=0.05) def test_batch_transform_dynamic_shots(self): """Tests that the batch transform considers the number of shots for the execution, not those @@ -462,10 +434,10 @@ def f(x): assert qml.math.allclose(out, expected) def test_single_backward_pass_split_hamiltonian(self): - """Tests that the backward pass is one single batch, not a bunch of batches, when parameter shift - derivatives are requested for a a tape that the device split into batches.""" + """Tests that the backward pass is one single batch, not a bunch of batches, when parameter + shift derivatives are requested for a tape that the device split into batches.""" - dev = qml.device("default.qubit.legacy", wires=2) + dev = qml.device("default.qubit.legacy", wires=2, shots=50000) H = qml.Hamiltonian([1, 1], [qml.PauliY(0), qml.PauliZ(0)], grouping_type="qwc") @@ -480,7 +452,7 @@ def f(x): assert dev.tracker.totals["batches"] == 2 assert dev.tracker.history["batch_len"] == [2, 4] - assert qml.math.allclose(out, -np.cos(x) - np.sin(x)) + assert qml.math.allclose(out, -np.cos(x) - np.sin(x), atol=0.05) execute_kwargs_integration = [ diff --git a/tests/interfaces/test_autograd_qnode.py b/tests/interfaces/test_autograd_qnode.py index b833e6e61b7..2417c9666c0 100644 --- a/tests/interfaces/test_autograd_qnode.py +++ b/tests/interfaces/test_autograd_qnode.py @@ -1666,7 +1666,7 @@ def test_hamiltonian_expansion_finite_shots( gradient_kwargs = {"h": 0.05} dev = qml.device(dev_name, wires=3, shots=50000) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(0) @ qml.PauliZ(1)] @qnode( diff --git a/tests/interfaces/test_jax_jit_qnode.py b/tests/interfaces/test_jax_jit_qnode.py index 2cd5c3c56dc..f109ddc0a4c 100644 --- a/tests/interfaces/test_jax_jit_qnode.py +++ b/tests/interfaces/test_jax_jit_qnode.py @@ -1540,7 +1540,7 @@ def test_hamiltonian_expansion_analytic( tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=3, shots=None) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(0) @ qml.PauliZ(1)] @qnode( @@ -1609,7 +1609,7 @@ def test_hamiltonian_expansion_finite_shots( tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=3, shots=50000) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(0) @ qml.PauliZ(1)] @qnode( diff --git a/tests/interfaces/test_jax_qnode.py b/tests/interfaces/test_jax_qnode.py index 0803ff10e79..fa88e315155 100644 --- a/tests/interfaces/test_jax_qnode.py +++ b/tests/interfaces/test_jax_qnode.py @@ -1493,7 +1493,7 @@ def test_hamiltonian_expansion_analytic( tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=3, shots=None) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(0) @ qml.PauliZ(1)] @qnode( @@ -1566,7 +1566,7 @@ def test_hamiltonian_expansion_finite_shots( tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=3, shots=50000) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(0) @ qml.PauliZ(1)] @qnode( diff --git a/tests/interfaces/test_tensorflow_qnode.py b/tests/interfaces/test_tensorflow_qnode.py index c07526eb514..5b03f9da10f 100644 --- a/tests/interfaces/test_tensorflow_qnode.py +++ b/tests/interfaces/test_tensorflow_qnode.py @@ -1414,7 +1414,7 @@ def test_hamiltonian_expansion_finite_shots( gradient_kwargs = {"h": 0.05} dev = qml.device(dev_name, wires=3, shots=50000) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(0) @ qml.PauliZ(1)] @qnode( diff --git a/tests/interfaces/test_torch_qnode.py b/tests/interfaces/test_torch_qnode.py index 6c5d807f594..ebbf2beee8a 100644 --- a/tests/interfaces/test_torch_qnode.py +++ b/tests/interfaces/test_torch_qnode.py @@ -1493,7 +1493,7 @@ def test_hamiltonian_expansion_finite_shots( np.random.seed(1235) dev = qml.device(dev_name, wires=3, shots=50000) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(0) @ qml.PauliZ(1)] @qnode( diff --git a/tests/test_device.py b/tests/test_device.py index a9337da8808..bb8df2374cb 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1174,9 +1174,9 @@ def test_batch_transform_checks_use_grouping_property(self, use_grouping, mocker H = qml.Hamiltonian([1.0, 1.0], [qml.PauliX(0), qml.PauliY(0)], grouping_type="qwc") qs = qml.tape.QuantumScript(measurements=[qml.expval(H)]) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") - dev = self.SomeDevice(shots=None) + dev = self.SomeDevice() dev.use_grouping = use_grouping new_qscripts, _ = dev.batch_transform(qs) @@ -1191,9 +1191,9 @@ def test_batch_transform_does_not_expand_supported_sum(self, mocker): """Tests that batch_transform does not expand Sums if they are supported.""" H = qml.sum(qml.PauliX(0), qml.PauliY(0)) qs = qml.tape.QuantumScript(measurements=[qml.expval(H)]) - spy = mocker.spy(qml.transforms, "sum_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") - dev = self.SomeDevice() + dev = self.SomeDevice(shots=None) new_qscripts, _ = dev.batch_transform(qs) assert len(new_qscripts) == 1 @@ -1203,7 +1203,7 @@ def test_batch_transform_expands_not_supported_sums(self, mocker): """Tests that batch_transform expand Sums if they are not supported.""" H = qml.sum(qml.PauliX(0), qml.PauliY(0)) qs = qml.tape.QuantumScript(measurements=[qml.expval(H)]) - spy = mocker.spy(qml.transforms, "sum_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") dev = self.SomeDevice() dev.supports_observable = lambda *args, **kwargs: False @@ -1217,7 +1217,7 @@ def test_batch_transform_expands_prod_containing_sums(self, mocker): H = qml.prod(qml.PauliX(0), qml.sum(qml.PauliY(0), qml.PauliZ(0))) qs = qml.tape.QuantumScript(measurements=[qml.expval(H)]) - spy = mocker.spy(qml.transforms, "sum_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") dev = self.SomeDevice() dev.supports_observable = lambda *args, **kwargs: False diff --git a/tests/test_qnode_legacy.py b/tests/test_qnode_legacy.py index 34f7623c37b..c98fc3f05fd 100644 --- a/tests/test_qnode_legacy.py +++ b/tests/test_qnode_legacy.py @@ -1850,7 +1850,7 @@ def test_hamiltonian_expansion_finite_shots(self, mocker): def circuit(): return qml.expval(H) - spy = mocker.spy(qml.transforms, "hamiltonian_expand") + spy = mocker.spy(qml.transforms, "split_non_commuting") res = circuit() assert np.allclose(res, c[2], atol=0.3) diff --git a/tests/test_vqe.py b/tests/test_vqe.py index 84248d35f3d..9c8d63ce1b9 100644 --- a/tests/test_vqe.py +++ b/tests/test_vqe.py @@ -64,18 +64,6 @@ def res(params): (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1), qml.PauliZ(1)), ] -with qml.operation.disable_new_opmath_cm(): - OBSERVABLES_LEGACY = [ - (qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)), - (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1), qml.PauliZ(1)), - (qml.Hermitian(H_TWO_QUBITS, [0, 1]),), - ] - - OBSERVABLES_NO_HERMITIAN_LEGACY = [ - (qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)), - (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1), qml.PauliZ(1)), - ] - hamiltonians_with_expvals = [ ((-0.6,), (qml.PauliZ(0),), [-0.6 * 1.0]), ((1.0,), (qml.PauliX(0),), [0.0]), @@ -274,6 +262,22 @@ def get_device(wires=1): add_queue = zip(QUEUE_HAMILTONIANS_1, QUEUE_HAMILTONIANS_2, QUEUES) +##################################################### +# Helper functions + + +def _convert_obs_to_legacy_opmath(obs): + """Convert single-term observables to legacy opmath""" + + if isinstance(obs, qml.ops.Prod): + return qml.operation.Tensor(*obs.operands) + + if isinstance(obs, (list, tuple)): + return [_convert_obs_to_legacy_opmath(o) for o in obs] + + return obs + + ##################################################### # Tests @@ -281,43 +285,25 @@ def get_device(wires=1): class TestVQE: """Test the core functionality of the VQE module""" - @pytest.mark.usefixtures("use_new_opmath") @pytest.mark.parametrize("ansatz, params", CIRCUITS) @pytest.mark.parametrize("coeffs, observables", list(zip(COEFFS, OBSERVABLES))) def test_cost_evaluate(self, params, ansatz, coeffs, observables): """Tests that the cost function evaluates properly""" + if not qml.operation.active_new_opmath(): + observables = _convert_obs_to_legacy_opmath(observables) hamiltonian = qml.Hamiltonian(coeffs, observables) dev = qml.device("default.qubit", wires=3) expval = generate_cost_fn(ansatz, hamiltonian, dev) assert expval(params).dtype == np.float64 assert np.shape(expval(params)) == () # expval should be scalar - @pytest.mark.usefixtures("use_legacy_opmath") - @pytest.mark.parametrize("ansatz, params", CIRCUITS) - @pytest.mark.parametrize("coeffs, observables", list(zip(COEFFS, OBSERVABLES_LEGACY))) - def test_cost_evaluate_legacy(self, params, ansatz, coeffs, observables): - """Tests that the cost function evaluates properly""" - hamiltonian = qml.Hamiltonian(coeffs, observables) - dev = qml.device("default.qubit", wires=3) - expval = generate_cost_fn(ansatz, hamiltonian, dev) - assert expval(params).dtype == np.float64 - assert np.shape(expval(params)) == () # expval should be scalar - - @pytest.mark.usefixtures("use_new_opmath") @pytest.mark.parametrize( "coeffs, observables, expected", hamiltonians_with_expvals + zero_hamiltonians_with_expvals ) def test_cost_expvals(self, coeffs, observables, expected): """Tests that the cost function returns correct expectation values""" - dev = qml.device("default.qubit", wires=2) - hamiltonian = qml.Hamiltonian(coeffs, observables) - cost = generate_cost_fn(lambda params, **kwargs: None, hamiltonian, dev) - assert cost([]) == sum(expected) - - @pytest.mark.usefixtures("use_legacy_opmath") - @pytest.mark.parametrize("coeffs, observables, expected", hamiltonians_with_expvals) - def test_cost_expvals_legacy(self, coeffs, observables, expected): - """Tests that the cost function returns correct expectation values""" + if not qml.operation.active_new_opmath() and (not coeffs or all(c == 0 for c in coeffs)): + pytest.skip("Legacy opmath does not support zero Hamiltonians") dev = qml.device("default.qubit", wires=2) hamiltonian = qml.Hamiltonian(coeffs, observables) cost = generate_cost_fn(lambda params, **kwargs: None, hamiltonian, dev) @@ -371,19 +357,23 @@ def test_optimize_torch(self, dev_name, shots): exec_no_opt = tracker.totals["executions"] assert exec_opt == 5 # Number of groups in the Hamiltonian - assert exec_no_opt == 14 + assert exec_no_opt == 8 # Number of wire-based groups assert np.allclose(c1, c2, atol=1e-1) # pylint: disable=protected-access @pytest.mark.tf @pytest.mark.slow + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.qubit.legacy"]) @pytest.mark.parametrize("shots", [None, [(8000, 5)], [(8000, 5), (9000, 4)]]) - def test_optimize_tf(self, shots): + def test_optimize_tf(self, shots, dev_name): """Test that a Hamiltonian cost function is the same with and without grouping optimization when using the TensorFlow interface.""" - dev = qml.device("default.qubit", wires=4, shots=shots) + if dev_name == "default.qubit.legacy" and shots is None: + pytest.xfail(reason="DQ legacy does not count hardware executions in analytic mode") + + dev = qml.device(dev_name, wires=4, shots=shots) hamiltonian1 = copy.copy(big_hamiltonian) hamiltonian2 = copy.copy(big_hamiltonian) @@ -417,19 +407,23 @@ def test_optimize_tf(self, shots): exec_no_opt = tracker.totals["executions"] assert exec_opt == 5 # Number of groups in the Hamiltonian - assert exec_no_opt == 14 + assert exec_no_opt == 8 # Number of wire-based groups assert np.allclose(c1, c2, atol=1e-1) # pylint: disable=protected-access @pytest.mark.autograd @pytest.mark.slow + @pytest.mark.parametrize("dev_name", ["default.qubit", "default.qubit.legacy"]) @pytest.mark.parametrize("shots", [None, [(8000, 5)], [(8000, 5), (9000, 4)]]) - def test_optimize_autograd(self, shots): + def test_optimize_autograd(self, shots, dev_name): """Test that a Hamiltonian cost function is the same with and without grouping optimization when using the autograd interface.""" - dev = qml.device("default.qubit", wires=4, shots=shots) + if dev_name == "default.qubit.legacy" and shots is None: + pytest.xfail(reason="DQ legacy does not count hardware executions in analytic mode") + + dev = qml.device(dev_name, wires=4, shots=shots) hamiltonian1 = copy.copy(big_hamiltonian) hamiltonian2 = copy.copy(big_hamiltonian) @@ -463,7 +457,7 @@ def test_optimize_autograd(self, shots): exec_no_opt = tracker.totals["executions"] assert exec_opt == 5 # Number of groups in the Hamiltonian - assert exec_no_opt == 14 + assert exec_no_opt == 8 assert np.allclose(c1, c2, atol=1e-1) @@ -519,7 +513,7 @@ def test_optimize_multiple_terms_autograd(self): exec_no_opt = tracker.totals["executions"] assert exec_opt == 1 # Number of groups in the Hamiltonian - assert exec_no_opt == 8 + assert exec_no_opt == 4 # number of wire-based groups assert np.allclose(c1, c2) @@ -574,9 +568,8 @@ def test_optimize_multiple_terms_torch(self): c2 = cost2(w) exec_no_opt = tracker.totals["executions"] - # was 1, 8 on old device assert exec_opt == 1 # Number of groups in the Hamiltonian - assert exec_no_opt == 8 + assert exec_no_opt == 4 assert np.allclose(c1, c2) @@ -631,9 +624,8 @@ def test_optimize_multiple_terms_tf(self): c2 = cost2(w) exec_no_opt = tracker.totals["executions"] - # was 1, 8 on old device assert exec_opt == 1 # Number of groups in the Hamiltonian - assert exec_no_opt == 8 + assert exec_no_opt == 4 assert np.allclose(c1, c2) @@ -772,43 +764,14 @@ class TestNewVQE: """Test the new VQE syntax of passing the Hamiltonian as an observable.""" # pylint: disable=cell-var-from-loop - @pytest.mark.usefixtures("use_new_opmath") @pytest.mark.parametrize("ansatz, params", CIRCUITS) @pytest.mark.parametrize("observables", OBSERVABLES_NO_HERMITIAN) def test_circuits_evaluate(self, ansatz, observables, params, tol): """Tests simple VQE evaluations.""" - coeffs = [1.0] * len(observables) - dev = qml.device("default.qubit", wires=3) - H = qml.Hamiltonian(coeffs, observables) - # pass H directly - @qml.qnode(dev) - def circuit(): - ansatz(params, wires=range(3)) - return qml.expval(H) - - res = circuit() + if not qml.operation.active_new_opmath(): + observables = _convert_obs_to_legacy_opmath(observables) - res_expected = [] - for obs in observables: - - @qml.qnode(dev) - def separate_circuit(): - ansatz(params, wires=range(3)) - return qml.expval(obs) - - res_expected.append(separate_circuit()) - - res_expected = np.sum([c * r for c, r in zip(coeffs, res_expected)]) - - assert np.isclose(res, res_expected, atol=tol) - - # pylint: disable=cell-var-from-loop - @pytest.mark.usefixtures("use_legacy_opmath") - @pytest.mark.parametrize("ansatz, params", CIRCUITS) - @pytest.mark.parametrize("observables", OBSERVABLES_NO_HERMITIAN_LEGACY) - def test_circuits_evaluate_legacy(self, ansatz, observables, params, tol): - """Tests simple VQE evaluations.""" coeffs = [1.0] * len(observables) dev = qml.device("default.qubit", wires=3) H = qml.Hamiltonian(coeffs, observables) @@ -959,7 +922,7 @@ def circuit1(): # the LinearCombination implementation does have diagonalizing gates, # but legacy Hamiltonian does not and fails - @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.usefixtures("legacy_opmath_only") def test_error_var_measurement(self): """Tests that error is thrown if var(H) is measured.""" observables = [qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)] @@ -976,7 +939,7 @@ def circuit(): # the LinearCombination implementation does have diagonalizing gates, # but legacy Hamiltonian does not and fails - @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.usefixtures("legacy_opmath_only") def test_error_sample_measurement(self): """Tests that error is thrown if sample(H) is measured.""" observables = [qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)] @@ -1086,7 +1049,7 @@ def circuit(w): dc = jax.grad(circuit)(w) assert np.allclose(dc, big_hamiltonian_grad, atol=tol) - @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.usefixtures("legacy_opmath_only") def test_specs_legacy(self): """Test that the specs of a VQE circuit can be computed""" dev = qml.device("default.qubit", wires=2) @@ -1110,7 +1073,7 @@ def circuit(): @pytest.mark.xfail( reason="diagonalizing gates defined but not used, should not be included in specs" ) - @pytest.mark.usefixtures("use_new_opmath") + @pytest.mark.usefixtures("new_opmath_only") def test_specs(self): """Test that the specs of a VQE circuit can be computed""" dev = qml.device("default.qubit", wires=2) @@ -1131,7 +1094,7 @@ def circuit(): # to be revisited in [sc-59117] assert res["num_diagonalizing_gates"] == 0 - @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.usefixtures("legacy_opmath_only") def test_specs_legacy_opmath(self): """Test that the specs of a VQE circuit can be computed""" dev = qml.device("default.qubit", wires=2)