diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py index 5401b230ac3..fbc30a47cf4 100644 --- a/pennylane/ops/op_math/sum.py +++ b/pennylane/ops/op_math/sum.py @@ -525,8 +525,9 @@ def compute_grouping(self, grouping_type="qwc", method="lf"): _, ops = self.terms() with qml.QueuingManager.stop_recording(): - op_groups = qml.pauli.group_observables(ops, grouping_type=grouping_type, method=method) - self._grouping_indices = tuple(tuple(ops.index(o) for o in group) for group in op_groups) + self._grouping_indices = qml.pauli.compute_partition_indices( + ops, grouping_type=grouping_type, method=method + ) @property def coeffs(self): diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 77f7967e23a..a1e287f0933 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -57,6 +57,9 @@ class Hadamard(Observable, Operation): _queue_category = "_ops" + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + def label( self, decimals: Optional[int] = None, diff --git a/pennylane/transforms/split_non_commuting.py b/pennylane/transforms/split_non_commuting.py index c940e40cc9d..1faab45de73 100644 --- a/pennylane/transforms/split_non_commuting.py +++ b/pennylane/transforms/split_non_commuting.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.measurements import ExpectationMP, MeasurementProcess, Shots, StateMP -from pennylane.ops import LinearCombination, Prod, SProd, Sum +from pennylane.ops import Prod, SProd, Sum from pennylane.tape import QuantumScript, QuantumScriptBatch from pennylane.transforms import transform from pennylane.typing import PostprocessingFn, Result, ResultBatch, TensorLike, Union @@ -279,7 +279,7 @@ def circuit(x): if grouping_strategy is None: measurements = list(single_term_obs_mps.keys()) - tapes = [tape.__class__(tape.operations, [m], shots=tape.shots) for m in measurements] + tapes = [tape.copy(measurements=[m]) for m in measurements] return tapes, partial( _processing_fn_no_grouping, single_term_obs_mps=single_term_obs_mps, @@ -288,26 +288,15 @@ def circuit(x): batch_size=tape.batch_size, ) - if ( - grouping_strategy == "wires" - or grouping_strategy == "default" - and any( - isinstance(m, ExpectationMP) and isinstance(m.obs, LinearCombination) - for m in tape.measurements - ) - or any( - m.obs is not None and not qml.pauli.is_pauli_word(m.obs) for m in single_term_obs_mps - ) + if grouping_strategy == "wires" or any( + m.obs is not None and not qml.pauli.is_pauli_word(m.obs) for m in single_term_obs_mps ): - # This is a loose check to see whether wires grouping or qwc grouping should be used, - # which does not necessarily make perfect sense but is consistent with the old decision - # logic in `Device.batch_transform`. The premise is that qwc grouping is classically - # expensive but produces fewer tapes, whereas wires grouping is classically faster to - # compute, but inefficient quantum-wise. If this transform is to be added to a device's - # `preprocess`, it will be performed for every circuit execution, which can get very - # expensive if there is a large number of observables. The reasoning here is, large - # Hamiltonians typically come in the form of a `LinearCombination`, so - # if we see one of those, use wires grouping to be safe. Otherwise, use qwc grouping. + # TODO: here we fall back to wire-based grouping if any of the observables in the tape + # is not a pauli word. As a result, adding a single measurement to a circuit could + # significantly increase the number of circuit executions. We should be able to + # separate the logic for pauli-word observables and non-pauli-word observables, + # putting non-pauli-word observables in separate wire-based groups, but using qwc + # based grouping for the rest of the observables. [sc-79686] return _split_using_wires_grouping(tape, single_term_obs_mps, offsets) return _split_using_qwc_grouping(tape, single_term_obs_mps, offsets) @@ -370,7 +359,7 @@ def _split_ham_with_grouping(tape: qml.tape.QuantumScript): mp_groups.append(mp_group) group_sizes.append(group_size) - tapes = [tape.__class__(tape.operations, mps, shots=tape.shots) for mps in mp_groups] + tapes = [tape.copy(measurements=mps) for mps in mp_groups] return tapes, partial( _processing_fn_with_grouping, single_term_obs_mps=single_term_obs_mps, @@ -405,7 +394,7 @@ def _split_using_qwc_grouping( obs_list = [_mp_to_obs(m, tape) for m in measurements] index_groups = [] if len(obs_list) > 0: - _, index_groups = qml.pauli.group_observables(obs_list, range(len(obs_list))) + index_groups = qml.pauli.compute_partition_indices(obs_list) # A dictionary for measurements of each unique single-term observable, mapped to the # indices of the original measurements it belongs to, its coefficients, the index of @@ -436,7 +425,7 @@ def _split_using_qwc_grouping( ) group_sizes.append(1) - tapes = [tape.__class__(tape.operations, mps, shots=tape.shots) for mps in mp_groups] + tapes = [tape.copy(measurements=mps) for mps in mp_groups] return tapes, partial( _processing_fn_with_grouping, single_term_obs_mps=single_term_obs_mps_grouped, @@ -507,7 +496,7 @@ def _split_using_wires_grouping( single_term_obs_mps_grouped[smp] = (mp_indices, coeffs, num_groups, 0) num_groups += 1 - tapes = [tape.__class__(tape.operations, mps, shots=tape.shots) for mps in mp_groups] + tapes = [tape.copy(measurements=mps) for mps in mp_groups] return tapes, partial( _processing_fn_with_grouping, single_term_obs_mps=single_term_obs_mps_grouped, @@ -558,8 +547,10 @@ def _split_all_multi_term_obs_mps(tape: qml.tape.QuantumScript): # Otherwise, add this new measurement to the list of single-term measurements. else: single_term_obs_mps[sm] = ([mp_idx], [c]) + elif isinstance(obs, qml.Identity): + offset += 1 else: - if isinstance(obs, SProd): + if isinstance(obs, (SProd, Prod)): obs = obs.simplify() if isinstance(obs, Sum): raise RuntimeError( @@ -606,27 +597,7 @@ def _processing_fn_no_grouping( res_batch_for_each_mp[mp_idx].append(res[smp_idx]) coeffs_for_each_mp[mp_idx].append(coeff) - result_shape = _infer_result_shape(shots, batch_size) - - # Sum up the results for each original measurement - res_for_each_mp = [ - _sum_terms(_sub_res, coeffs, offset, result_shape) - for _sub_res, coeffs, offset in zip(res_batch_for_each_mp, coeffs_for_each_mp, offsets) - ] - - # res_for_each_mp should have shape (n_mps, [,n_shots] [,batch_size]) - if len(res_for_each_mp) == 1: - return res_for_each_mp[0] - - if shots.has_partitioned_shots: - # If the shot vector dimension exists, it should be moved to the first axis - # Basically, the shape becomes (n_shots, n_mps, [,batch_size]) - res_for_each_mp = [ - tuple(res_for_each_mp[j][i] for j in range(len(res_for_each_mp))) - for i in range(shots.num_copies) - ] - - return tuple(res_for_each_mp) + return _res_for_each_mp(res_batch_for_each_mp, coeffs_for_each_mp, offsets, shots, batch_size) def _processing_fn_with_grouping( @@ -678,6 +649,12 @@ def _processing_fn_with_grouping( res_batch_for_each_mp[mp_idx].append(sub_res) coeffs_for_each_mp[mp_idx].append(coeff) + return _res_for_each_mp(res_batch_for_each_mp, coeffs_for_each_mp, offsets, shots, batch_size) + + +def _res_for_each_mp(res_batch_for_each_mp, coeffs_for_each_mp, offsets, shots, batch_size): + """Helper function that combines a result batch into results for each mp""" + result_shape = _infer_result_shape(shots, batch_size) # Sum up the results for each original measurement diff --git a/tests/measurements/test_classical_shadow.py b/tests/measurements/test_classical_shadow.py index 90b2b76e2f8..0340592c1da 100644 --- a/tests/measurements/test_classical_shadow.py +++ b/tests/measurements/test_classical_shadow.py @@ -497,7 +497,7 @@ def test_multi_measurement_allowed(self, seed): def circuit(): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) - return qml.shadow_expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(0)) + return qml.shadow_expval(qml.PauliZ(0), seed=seed), qml.expval(qml.PauliZ(0)) res = circuit() assert isinstance(res, tuple) diff --git a/tests/measurements/test_probs.py b/tests/measurements/test_probs.py index ac21dee24ae..41278b341a5 100644 --- a/tests/measurements/test_probs.py +++ b/tests/measurements/test_probs.py @@ -409,6 +409,7 @@ def test_observable_is_measurement_value_list( ): # pylint: disable=too-many-arguments """Test that probs for mid-circuit measurement values are correct for a measurement value list.""" + dev = qml.device("default.qubit", seed=seed) @qml.qnode(dev) diff --git a/tests/transforms/test_split_non_commuting.py b/tests/transforms/test_split_non_commuting.py index 7dd60342224..45ff25d6a92 100644 --- a/tests/transforms/test_split_non_commuting.py +++ b/tests/transforms/test_split_non_commuting.py @@ -12,11 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for the transform ``qml.transform.split_non_commuting`` """ - -# pylint: disable=import-outside-toplevel,unnecessary-lambda,too-many-arguments - -import itertools +"""Tests for the transform ``qml.transforms.split_non_commuting``""" from functools import partial import numpy as np @@ -25,7 +21,7 @@ import pennylane as qml from pennylane.transforms import split_non_commuting -# Two commuting groups: [[0, 3], [1, 2, 4]] +# Two qubit-wise commuting groups: [[0, 3], [1, 2, 4]] # Four groups based on wire overlaps: [[0, 2], [1], [3], [4]] single_term_obs_list = [ qml.X(0), @@ -33,6 +29,7 @@ qml.Z(1), qml.X(0) @ qml.Y(1), qml.Y(0) @ qml.Z(1), + qml.I(0), ] single_term_qwc_groups = [ @@ -48,7 +45,7 @@ ] # contains the following observables: X(0), Y(0), Y(0) @ Z(1), X(1), Z(1), X(0) @ Y(1) -# qwc groups: [[0, 5], [1, 3], [2, 4]] +# qwc groups: [[0, 5], [1, 3, 4], [2]] # wires groups: [[0, 3], [1, 4], [2], [5]] complex_obs_list = [ qml.X(0), # single observable @@ -57,7 +54,7 @@ qml.Hamiltonian( [0.1, 0.2, 0.3, 0.4], [qml.Z(1), qml.X(0) @ qml.Y(1), qml.Y(0) @ qml.Z(1), qml.I()] ), - 1.5 * qml.I(), # identity + 1.5 * qml.I(0), # identity ] complex_no_grouping_obs = [ @@ -95,7 +92,7 @@ def complex_qwc_processing_fn(results): return ( group0[0], 0.5 * group1[0], - group0[0] + group1[1] + 2.0 * group2[0] + 1.0, + group0[0] + group1[1] + 2.0 * group2 + 1.0, 0.1 * group1[2] + 0.2 * group0[1] + 0.3 * group1[1] + 0.4, 1.5, ) @@ -122,765 +119,591 @@ def complex_wires_processing_fn(results): ) -# Measurements that accept observables as arguments -obs_measurements = [qml.expval, qml.var, qml.probs, qml.counts, qml.sample] - -# measurements that accept wires as arguments -wire_measurements = [qml.probs, qml.counts, qml.sample] - +# contains the following observables: X(0), Y(0), Y(0) @ Z(1), X(1), H(0) @ X(1), Projector(1) +# wires groups: ((0, 3), (1, 5), (2,), (4,)) +non_pauli_obs_list = [ + qml.X(0), + 0.5 * qml.Y(0), + qml.X(0) + qml.Y(0) @ qml.Z(1) + 2.0 * qml.X(1) + qml.H(0) @ qml.PauliX(wires=[1]), + qml.Projector([0, 1], wires=1), +] -class TestUnits: - """Unit tests for components of the ``split_non_commuting`` transform""" +non_pauli_obs_wires_groups = [ + [qml.X(0), qml.X(1)], + [qml.Y(0), qml.Projector([0, 1], wires=1)], + [qml.Y(0) @ qml.Z(1)], + [qml.H(0) @ qml.X(1)], +] - @pytest.mark.parametrize("measure_fn", obs_measurements) - @pytest.mark.parametrize( - "grouping_strategy, n_tapes", [(None, 5), ("default", 2), ("qwc", 2), ("wires", 4)] - ) - def test_number_of_tapes(self, measure_fn, grouping_strategy, n_tapes): - """Tests that the correct number of tapes is returned""" - measurements = [measure_fn(op=o) for o in single_term_obs_list] - tape = qml.tape.QuantumScript([qml.X(0), qml.CNOT([0, 1])], measurements, shots=100) - tapes, _ = split_non_commuting(tape, grouping_strategy=grouping_strategy) - assert len(tapes) == n_tapes - assert all(t.operations == [qml.X(0), qml.CNOT([0, 1])] for t in tapes) - assert all(t.shots == tape.shots for t in tapes) +def non_pauli_obs_processing_fn(results): + """The expected processing function for non-Pauli observables""" - @pytest.mark.parametrize( - "grouping_strategy, n_tapes", [(None, 5), ("default", 2), ("qwc", 2), ("wires", 4)] - ) - @pytest.mark.parametrize( - "make_H", - [ - lambda coeffs, obs: qml.Hamiltonian(coeffs, obs), - lambda coeffs, obs: qml.sum(*(qml.s_prod(c, o) for c, o in zip(coeffs, obs))), - ], - ) - def test_number_of_tapes_single_hamiltonian(self, grouping_strategy, n_tapes, make_H): - """Tests that the correct number of tapes is returned for a single Hamiltonian""" + group0, group1, group2, group3 = results + res = (group0[0], 0.5 * group1[0], group0[0] + group2 + 2.0 * group0[1] + group3, group1[1]) + return res - obs_list = single_term_obs_list - obs_list = obs_list + [qml.Y(0), qml.X(0) @ qml.Y(1)] # add duplicate terms - coeffs = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7] +@pytest.mark.integration +class TestSplitNonCommuting: + """Tests the basic functionality of the split_non_commuting transform - H = make_H(coeffs, obs_list) - tape = qml.tape.QuantumScript([qml.X(0), qml.CNOT([0, 1])], [qml.expval(H)], shots=100) - tapes, _ = split_non_commuting(tape, grouping_strategy=grouping_strategy) - assert len(tapes) == n_tapes - assert all(t.operations == [qml.X(0), qml.CNOT([0, 1])] for t in tapes) - assert all(t.shots == tape.shots for t in tapes) + The ``split_non_commuting`` transform supports three different grouping strategies: + - wires: groups observables based on wire overlaps + - qwc: groups observables based on qubit-wise commuting groups + - None: no grouping (each observable is measured in a separate tape) - @pytest.mark.parametrize( - "grouping_strategy, n_tapes", [(None, 6), ("default", 4), ("qwc", 3), ("wires", 4)] - ) - def test_number_of_tapes_complex_obs(self, grouping_strategy, n_tapes): - """Tests number of tapes with mixed types of observables""" + The unit tests below test each grouping strategy separately. The tests in this test class + focus on whether the transform produces the correct tapes, and whether the processing function + is able to recombine the results correctly. - measurements = [qml.expval(o) for o in complex_obs_list] - tape = qml.tape.QuantumScript([qml.X(0), qml.CNOT([0, 1])], measurements, shots=100) - tapes, _ = split_non_commuting(tape, grouping_strategy=grouping_strategy) - assert len(tapes) == n_tapes - assert all(t.operations == [qml.X(0), qml.CNOT([0, 1])] for t in tapes) - assert all(t.shots == tape.shots for t in tapes) + """ - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - def test_state_measurement_in_separate_tape(self, grouping_strategy): - """Tests that a state measurement is in a separate tape""" + def test_tape_no_measurements(self): + """Tests that a tape with no measurements is returned unchanged.""" - measurements = [qml.expval(qml.Z(0) @ qml.Z(1)), qml.state()] - tape = qml.tape.QuantumScript([qml.X(0), qml.CNOT([0, 1])], measurements, shots=100) - tapes, _ = split_non_commuting(tape, grouping_strategy=grouping_strategy) - assert len(tapes) == 2 - assert all(t.operations == [qml.X(0), qml.CNOT([0, 1])] for t in tapes) - assert all(t.shots == tape.shots for t in tapes) + initial_tape = qml.tape.QuantumScript([qml.Z(0)], []) + tapes, fn = split_non_commuting(initial_tape) + assert tapes == [initial_tape] + assert fn([0]) == 0 - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) @pytest.mark.parametrize( - "make_H", + "H", [ - lambda obs_list: qml.Hamiltonian([0.1, 0.2, 0.3, 0.4, 0.5], obs_list), - lambda obs_list: qml.sum( - *(qml.s_prod(c, o) for c, o in zip([0.1, 0.2, 0.3, 0.4, 0.5], obs_list)) - ), + qml.Hamiltonian([0.1, 0.2, 0.3, 0.4, 0.5, 2.5], single_term_obs_list), + qml.dot([0.1, 0.2, 0.3, 0.4, 0.5, 2.5], single_term_obs_list), ], ) - def test_existing_grouping_used_for_single_hamiltonian(self, grouping_strategy, make_H): - """Tests that if a Hamiltonian has an existing grouping, it is used regardless of - what is requested through the ``grouping_strategy`` argument.""" - - obs_list = single_term_obs_list - - H = make_H(obs_list) - H.compute_grouping() - - tape = qml.tape.QuantumScript([qml.X(0), qml.CNOT([0, 1])], [qml.expval(H)], shots=100) - tapes, _ = split_non_commuting(tape, grouping_strategy=grouping_strategy) - assert len(tapes) == 2 - assert all(t.operations == [qml.X(0), qml.CNOT([0, 1])] for t in tapes) - assert all(t.shots == tape.shots for t in tapes) - - @pytest.mark.parametrize("measure_fn", obs_measurements) - def test_single_group(self, measure_fn): - """Tests when all measurements can be taken at the same time""" - - with qml.queuing.AnnotatedQueue() as q: - qml.PauliZ(0) - qml.Hadamard(0) - qml.CNOT((0, 1)) - measure_fn(op=qml.X(0)) - measure_fn(op=qml.Y(1)) - measure_fn(op=qml.Z(2)) - measure_fn(op=qml.X(0) @ qml.Y(1)) - measure_fn(op=qml.Y(1) @ qml.Z(2)) - - tape = qml.tape.QuantumScript.from_queue(q, shots=100) - tapes, fn = split_non_commuting(tape) - - assert len(tapes) == 1 - assert fn([[0.1, 0.2, 0.3, 0.4, 0.5]]) == (0.1, 0.2, 0.3, 0.4, 0.5) - - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - def test_single_observable(self, grouping_strategy): - """Tests a circuit that contains a single observable""" - - tape = qml.tape.QuantumScript([], [qml.expval(qml.X(0))]) - tapes, fn = split_non_commuting(tape, grouping_strategy=grouping_strategy) - assert len(tapes) == 1 - assert fn([0.1]) == 0.1 - - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - def test_single_hamiltonian_single_observable(self, grouping_strategy): - """Tests a circuit that contains a single observable""" - - tape = qml.tape.QuantumScript([], [qml.expval(qml.Hamiltonian([0.1], [qml.X(0)]))]) - tapes, fn = split_non_commuting(tape, grouping_strategy=grouping_strategy) - assert len(tapes) == 1 - assert qml.math.allclose(fn([0.1]), 0.01) - - @pytest.mark.parametrize("measure_fn", wire_measurements) - def test_all_wire_measurements(self, measure_fn): - """Tests that measurements based on wires don't need to be split""" - - with qml.queuing.AnnotatedQueue() as q: - qml.PauliZ(0) - qml.Hadamard(0) - qml.CNOT((0, 1)) - measure_fn() - measure_fn(wires=[0]) - measure_fn(wires=[1]) - measure_fn(wires=[0, 1]) - measure_fn(op=qml.PauliZ(0)) - measure_fn(op=qml.PauliZ(0) @ qml.PauliZ(2)) - - tape = qml.tape.QuantumScript.from_queue(q) - tapes, fn = split_non_commuting(tape) - - assert len(tapes) == 1 - assert fn([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]) == (0.1, 0.2, 0.3, 0.4, 0.5, 0.6) - - @pytest.mark.parametrize("obs_meas_1, obs_meas_2", itertools.combinations(obs_measurements, 2)) @pytest.mark.parametrize( - "wire_meas_1, wire_meas_2", itertools.combinations(wire_measurements, 2) - ) - def test_mix_measurement_types(self, obs_meas_1, obs_meas_2, wire_meas_1, wire_meas_2): - """Tests that tapes mixing different measurement types is handled correctly""" - - with qml.queuing.AnnotatedQueue() as q: - obs_meas_1(op=qml.PauliX(0)) - obs_meas_2(op=qml.PauliZ(1)) - obs_meas_1(op=qml.PauliZ(0)) - wire_meas_1(wires=[0]) - wire_meas_2(wires=[1]) - wire_meas_1(wires=[0, 1]) - - tape = qml.tape.QuantumScript.from_queue(q) - tapes, _ = split_non_commuting(tape) - assert len(tapes) == 2 - assert tapes[0].measurements == [ - obs_meas_1(op=qml.PauliX(0)), - obs_meas_2(op=qml.PauliZ(1)), - wire_meas_2(wires=[1]), - ] - assert tapes[1].measurements == [ - obs_meas_1(op=qml.PauliZ(0)), - wire_meas_1(wires=[0]), - wire_meas_1(wires=[0, 1]), - ] - - def test_grouping_strategies(self): - """Tests that the tape is split correctly for different grouping strategies""" - - measurements = [ - qml.expval(c * o) for c, o in zip([0.1, 0.2, 0.3, 0.4, 0.5], single_term_obs_list) - ] - tape = qml.tape.QuantumScript([], measurements, shots=100) - - expected_tapes_no_grouping = [ - qml.tape.QuantumScript([], [qml.expval(o)], shots=100) for o in single_term_obs_list - ] - - # qwc grouping produces [[0, 3], [1, 2, 4]] - expected_tapes_qwc_grouping = [ - qml.tape.QuantumScript([], [qml.expval(o) for o in group], shots=100) - for group in single_term_qwc_groups - ] - - # wires grouping produces [[0, 2], [1], [3], [4]] - expected_tapes_wires_grouping = [ - qml.tape.QuantumScript([], [qml.expval(o) for o in group], shots=100) - for group in single_term_wires_groups - ] - - tapes, fn = split_non_commuting(tape, grouping_strategy=None) - for actual_tape, expected_tape in zip(tapes, expected_tapes_no_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose(fn([0.1, 0.2, 0.3, 0.4, 0.5]), [0.01, 0.04, 0.09, 0.16, 0.25]) - - tapes, fn = split_non_commuting(tape, grouping_strategy="default") - - for actual_tape, expected_tape in zip(tapes, expected_tapes_qwc_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose(fn([[0.1, 0.2], [0.3, 0.4, 0.5]]), [0.01, 0.06, 0.12, 0.08, 0.25]) - - tapes, fn = split_non_commuting(tape, grouping_strategy="qwc") - for actual_tape, expected_tape in zip(tapes, expected_tapes_qwc_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose(fn([[0.1, 0.2], [0.3, 0.4, 0.5]]), [0.01, 0.06, 0.12, 0.08, 0.25]) - - tapes, fn = split_non_commuting(tape, grouping_strategy="wires") - for actual_tape, expected_tape in zip(tapes, expected_tapes_wires_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose(fn([[0.1, 0.2], 0.3, 0.4, 0.5]), [0.01, 0.06, 0.06, 0.16, 0.25]) - - @pytest.mark.parametrize( - "make_H", + "grouping_indices, results, expected_result", [ - lambda coeffs, obs_list: qml.Hamiltonian(coeffs, obs_list), - lambda coeffs, obs_list: qml.sum(*(qml.s_prod(c, o) for c, o in zip(coeffs, obs_list))), + ( + [[0, 2, 5], [1], [3], [4]], + [[0.6, 0.7], [0.8], [0.9], [1]], + 0.6 * 0.1 + 0.7 * 0.3 + 0.8 * 0.2 + 0.9 * 0.4 + 0.5 + 2.5, + ), + ( + [[0, 3, 5], [1, 2, 4]], + [[0.6, 0.7], [0.8, 0.9, 1]], + 0.6 * 0.1 + 0.7 * 0.4 + 0.8 * 0.2 + 0.9 * 0.3 + 0.5 + 2.5, + ), ], ) - def test_grouping_strategies_single_hamiltonian(self, make_H): - """Tests that a single Hamiltonian or Sum is split correctly""" - - coeffs = [0.1, 0.2, 0.3, 0.4, 0.5] - obs_list = single_term_obs_list - H = make_H(coeffs, obs_list) # Tests that constant offset is handled - - expected_tapes_no_grouping = [ - qml.tape.QuantumScript([], [qml.expval(o)], shots=100) for o in obs_list - ] - - expected_tapes_qwc_grouping = [ - qml.tape.QuantumScript([], [qml.expval(o) for o in group], shots=100) - for group in single_term_qwc_groups - ] - - coeffs, obs_list = coeffs + [0.6], obs_list + [qml.I()] - H = make_H(coeffs, obs_list) # Tests that constant offset is handled + def test_single_hamiltonian_precomputed_grouping( + self, H, grouping_indices, results, expected_result + ): + """Tests that precomputed grouping of a single Hamiltonian is used.""" - tape = qml.tape.QuantumScript([], [qml.expval(H)], shots=100) + H.grouping_indices = grouping_indices # pylint: disable=protected-access + initial_tape = qml.tape.QuantumScript([qml.X(0)], [qml.expval(H)], shots=100) + tapes, fn = split_non_commuting(initial_tape) + assert len(tapes) == len(grouping_indices) - tapes, fn = split_non_commuting(tape, grouping_strategy=None) - for actual_tape, expected_tape in zip(tapes, expected_tapes_no_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose(fn([0.1, 0.2, 0.3, 0.4, 0.5]), 1.15) + for group_idx, group in enumerate(grouping_indices): + ob_group = [single_term_obs_list[i] for i in group] + obs_no_identity = [obs for obs in ob_group if not isinstance(obs, qml.Identity)] + expected_measurements = [qml.expval(obs) for obs in obs_no_identity] + assert tapes[group_idx].measurements == expected_measurements + assert tapes[group_idx].shots.total_shots == 100 + assert tapes[group_idx].operations == [qml.X(0)] - tapes, fn = split_non_commuting(tape, grouping_strategy="default") - for actual_tape, expected_tape in zip(tapes, expected_tapes_qwc_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose(fn([[0.1, 0.2], [0.3, 0.4, 0.5]]), 1.12) + assert fn(results) == expected_result @pytest.mark.parametrize( "H", [ - qml.sum(qml.X(0), qml.Hadamard(1) @ qml.Z(0), qml.Y(1)), - qml.Hamiltonian([1, 2, 3], [qml.X(0), qml.Hadamard(1) @ qml.Z(0), qml.Y(1)]), + # A duplicate term is added to the tests below to verify that it is handled correctly. + qml.Hamiltonian([0.1, 0.2, 0.3, 0.4, 0.5, 2.5, 0.8], single_term_obs_list + [qml.X(0)]), + qml.dot([0.1, 0.2, 0.3, 0.4, 0.5, 2.5, 0.8], single_term_obs_list + [qml.X(0)]), ], ) - def test_single_hamiltonian_non_pauli_words(self, H): - """Tests that a single Hamiltonian with non-pauli words is split correctly""" - - tape = qml.tape.QuantumScript([], [qml.expval(H)], shots=100) - tapes, _ = split_non_commuting(tape) - expected_tapes = [ - qml.tape.QuantumScript([], [qml.expval(qml.X(0)), qml.expval(qml.Y(1))], shots=100), - qml.tape.QuantumScript([], [qml.expval(qml.Hadamard(1) @ qml.Z(0))], shots=100), - ] - for actual_tape, expected_tape in zip(tapes, expected_tapes): - qml.assert_equal(actual_tape, expected_tape) + @pytest.mark.parametrize("grouping_strategy", ["qwc", "default"]) + def test_single_hamiltonian_grouping(self, H, grouping_strategy): + """Tests that a single Hamiltonian is split correctly.""" + + initial_tape = qml.tape.QuantumScript([qml.X(0)], [qml.expval(H)], shots=100) + tapes, fn = split_non_commuting(initial_tape, grouping_strategy=grouping_strategy) + assert H.grouping_indices is not None # H.grouping_indices should be computed by now. + + groups_without_duplicates = [] + for group in H.grouping_indices: + # The actual groups should not contain the added duplicate item + groups_without_duplicates.append([i for i in group if i != 6]) + + obs_list = single_term_obs_list + [qml.X(0)] + for group_idx, group in enumerate(groups_without_duplicates): + ob_group = [obs_list[i] for i in group] + obs_no_identity = [obs for obs in ob_group if not isinstance(obs, qml.Identity)] + expected_measurements = [qml.expval(obs) for obs in obs_no_identity] + assert tapes[group_idx].measurements == expected_measurements + assert tapes[group_idx].shots.total_shots == 100 + assert tapes[group_idx].operations == [qml.X(0)] + + assert ( + fn([[0.6, 0.7], [0.8, 0.9, 1]]) + == 0.6 * 0.1 + 0.7 * 0.4 + 0.8 * 0.2 + 0.9 * 0.3 + 0.5 + 2.5 + 0.8 * 0.6 + ) @pytest.mark.parametrize( - "grouping_strategy, expected_tapes, processing_fn, mock_results", + "obs, terms, results, expected_result", [ ( - None, - [ - qml.tape.QuantumScript([], [qml.expval(o)], shots=100) - for o in complex_no_grouping_obs - ], - complex_no_grouping_processing_fn, - [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], + [qml.Hamiltonian([0.1, 0.2, 0.3, 0.4, 0.5, 2.5], single_term_obs_list)], + single_term_obs_list[:-1], + [0.6, 0.7, 0.8, 0.9, 1], + 0.6 * 0.1 + 0.7 * 0.2 + 0.8 * 0.3 + 0.9 * 0.4 + 0.5 + 2.5, ), ( - "wires", - [ - qml.tape.QuantumScript([], [qml.expval(o) for o in group], shots=100) - for group in complex_wires_groups - ], - complex_wires_processing_fn, - [[0.1, 0.2], [0.3, 0.4], 0.5, 0.6], + single_term_obs_list, + single_term_obs_list[:-1], + [0.1, 0.2, 0.3, 0.4, 0.5], + [0.1, 0.2, 0.3, 0.4, 0.5, 1.0], ), ( - "qwc", - [ - qml.tape.QuantumScript([], [qml.expval(o) for o in group], shots=100) - for group in complex_qwc_groups - ], - complex_qwc_processing_fn, - [[0.1, 0.2], [0.3, 0.5, 0.6], [0.4]], + complex_obs_list, + complex_no_grouping_obs, + [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], + complex_no_grouping_processing_fn([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]), ), ], ) - def test_grouping_strategies_complex( - self, grouping_strategy, expected_tapes, processing_fn, mock_results - ): - """Tests that the tape is split correctly when containing more complex observables""" - - obs_list = complex_obs_list - - measurements = [qml.expval(o) for o in obs_list] - tape = qml.tape.QuantumScript([], measurements, shots=100) - tapes, fn = split_non_commuting(tape, grouping_strategy=grouping_strategy) + def test_no_grouping(self, obs, terms, results, expected_result): + """Tests splitting each observable into a separate tape.""" - for actual_tape, expected_tape in zip(tapes, expected_tapes): - qml.assert_equal(actual_tape, expected_tape) - - expected = processing_fn(mock_results) - - assert qml.math.allclose(fn(mock_results), expected) - - @pytest.mark.parametrize("batch_type", (tuple, list)) - def test_batch_of_tapes(self, batch_type): - """Test that `split_non_commuting` can transform a batch of tapes""" - - tape_batch = batch_type( - [ - qml.tape.QuantumScript( - [qml.RX(1.2, 0)], - [qml.expval(qml.X(0)), qml.expval(qml.Y(0)), qml.expval(qml.X(1))], - ), - qml.tape.QuantumScript( - [qml.RY(0.5, 0)], [qml.expval(qml.Z(0)), qml.expval(qml.Y(0))] - ), - ] + initial_tape = qml.tape.QuantumScript( + [qml.X(0)], measurements=[qml.expval(o) for o in obs], shots=100 ) - tapes, fn = split_non_commuting(tape_batch) - - expected_tapes = [ - qml.tape.QuantumScript([qml.RX(1.2, 0)], [qml.expval(qml.X(0)), qml.expval(qml.X(1))]), - qml.tape.QuantumScript([qml.RX(1.2, 0)], [qml.expval(qml.Y(0))]), - qml.tape.QuantumScript([qml.RY(0.5, 0)], [qml.expval(qml.Z(0))]), - qml.tape.QuantumScript([qml.RY(0.5, 0)], [qml.expval(qml.Y(0))]), - ] - for actual_tape, expected_tape in zip(tapes, expected_tapes): - qml.assert_equal(actual_tape, expected_tape) + tapes, fn = split_non_commuting(initial_tape, grouping_strategy=None) + assert len(tapes) == len(terms) + for tape, term in zip(tapes, terms): + assert tape.measurements == [qml.expval(term)] + assert tape.shots.total_shots == 100 + assert tape.operations == [qml.X(0)] - result = ([0.1, 0.2], 0.2, 0.3, 0.4) - assert fn(result) == ((0.1, 0.2, 0.2), (0.3, 0.4)) + assert qml.math.allclose(fn(results), expected_result) @pytest.mark.parametrize( - "non_pauli_obs", + "obs, groups, results, expected_result, grouping_strategy", [ - [ - qml.Projector([0], wires=[1]), - qml.Projector([1, 1, 0, 1], wires=[0, 1]), - ], - [ - qml.Hadamard(wires=[1]), - qml.Hadamard(wires=[0]) @ qml.PauliX(wires=[1]), - ], + # The following tests should route to wire-based grouping. + ( + [qml.Hamiltonian([0.1, 0.2, 0.3, 0.4, 0.5, 2.5], single_term_obs_list)], + single_term_wires_groups, + [[0.6, 0.7], 0.8, 0.9, 1], + 0.6 * 0.1 + 0.7 * 0.3 + 0.8 * 0.2 + 0.9 * 0.4 + 0.5 + 2.5, + "wires", + ), + ( + single_term_obs_list, + single_term_wires_groups, + [[0.1, 0.2], 0.3, 0.4, 0.5], + [0.1, 0.3, 0.2, 0.4, 0.5, 1.0], + "wires", + ), + ( + complex_obs_list, + complex_wires_groups, # [[0, 3], [1, 4], [2], [5]] + [[0.1, 0.2], [0.3, 0.4], 0.5, 0.6], + complex_wires_processing_fn([[0.1, 0.2], [0.3, 0.4], 0.5, 0.6]), + "wires", + ), + ( + non_pauli_obs_list, + non_pauli_obs_wires_groups, + [[0.1, 0.2], [0.3, 0.4], 0.5, 0.6], + non_pauli_obs_processing_fn([[0.1, 0.2], [0.3, 0.4], 0.5, 0.6]), + "default", # wire-based grouping should be automatically chosen + ), + # The following tests should route to qwc grouping. + ( + [qml.Hamiltonian([0.1, 0.2, 0.3, 0.4, 0.5, 2.5], single_term_obs_list)], + single_term_qwc_groups, # [[0, 3], [1, 2, 4]] + [[0.6, 0.7], [0.8, 0.9, 1]], + 0.6 * 0.1 + 0.7 * 0.4 + 0.8 * 0.2 + 0.9 * 0.3 + 0.5 + 2.5, + "default", # qwc grouping should be the default in this case. + ), + ( + [qml.Hamiltonian([0.1, 0.2, 0.3, 0.4, 0.5, 2.5], single_term_obs_list)], + single_term_qwc_groups, # [[0, 3], [1, 2, 4]] + [[0.6, 0.7], [0.8, 0.9, 1]], + 0.6 * 0.1 + 0.7 * 0.4 + 0.8 * 0.2 + 0.9 * 0.3 + 0.5 + 2.5, + "qwc", + ), + ( + single_term_obs_list, + single_term_qwc_groups, + [[0.6, 0.7], [0.8, 0.9, 1]], + [0.6, 0.8, 0.9, 0.7, 1.0, 1.0], + "default", # qwc grouping should be the default in this case. + ), + ( + single_term_obs_list, + single_term_qwc_groups, + [[0.6, 0.7], [0.8, 0.9, 1]], + [0.6, 0.8, 0.9, 0.7, 1.0, 1.0], + "qwc", + ), + ( + complex_obs_list, + complex_qwc_groups, # [[0, 5], [1, 3, 4], [2]] + [[0.1, 0.2], [0.3, 0.4, 0.5], 0.6], + complex_qwc_processing_fn([[0.1, 0.2], [0.3, 0.4, 0.5], 0.6]), + "qwc", + ), + ( + complex_obs_list, + complex_qwc_groups, + [[0.1, 0.2], [0.3, 0.4, 0.5], 0.6], + complex_qwc_processing_fn([[0.1, 0.2], [0.3, 0.4, 0.5], 0.6]), + "default", # qwc grouping should be automatically chosen for this case. + ), ], ) - def test_tape_with_non_pauli_obs(self, non_pauli_obs): - """Tests that the tape is split correctly when containing non-Pauli observables""" + def test_grouping_strategies( + self, obs, groups, results, expected_result, grouping_strategy + ): # pylint: disable=too-many-arguments + """Tests wire-based grouping and qwc grouping.""" - obs_list = single_term_obs_list + non_pauli_obs - - measurements = [ - qml.expval(c * o) for c, o in zip([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7], obs_list) + initial_tape = qml.tape.QuantumScript( + [qml.X(0)], measurements=[qml.expval(o) for o in obs], shots=100 + ) + tapes, fn = split_non_commuting(initial_tape, grouping_strategy=grouping_strategy) + assert len(tapes) == len(groups) + for tape, group in zip(tapes, groups): + assert tape.measurements == [qml.expval(term) for term in group] + assert tape.shots.total_shots == 100 + assert tape.operations == [qml.X(0)] + + assert qml.math.allclose(fn(results), expected_result) + + @pytest.mark.parametrize("grouping_strategy", ["wires", "qwc"]) + def test_single_group(self, grouping_strategy): + """Tests when all measurements can be taken at the same time.""" + + initial_tape = qml.tape.QuantumScript( + [qml.X(0)], + measurements=[qml.expval(qml.X(0)), qml.expval(qml.Y(1) + qml.Z(2))], + shots=100, + ) + tapes, fn = split_non_commuting(initial_tape, grouping_strategy=grouping_strategy) + assert len(tapes) == 1 + assert tapes[0].measurements == [ + qml.expval(qml.X(0)), + qml.expval(qml.Y(1)), + qml.expval(qml.Z(2)), ] - tape = qml.tape.QuantumScript([], measurements, shots=100) + assert tapes[0].shots.total_shots == 100 + assert tapes[0].operations == [qml.X(0)] + assert qml.math.allclose(fn([[0.1, 0.2, 0.3]]), [0.1, 0.2 + 0.3]) - expected_tapes_no_grouping = [ - qml.tape.QuantumScript([], [qml.expval(o)], shots=100) for o in obs_list - ] + @pytest.mark.parametrize("grouping_strategy", ["wires", "qwc", None]) + def test_single_observable(self, grouping_strategy): + """Tests a tape containing measurements of a single observable.""" - tapes, fn = split_non_commuting(tape, grouping_strategy=None) - for actual_tape, expected_tape in zip(tapes, expected_tapes_no_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose( - fn([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]), [0.01, 0.04, 0.09, 0.16, 0.25, 0.36, 0.49] + initial_tape = qml.tape.QuantumScript( + [qml.X(0)], measurements=[qml.expval(qml.Z(0)), qml.expval(0.5 * qml.Z(0))], shots=100 + ) + tapes, fn = split_non_commuting(initial_tape, grouping_strategy=grouping_strategy) + assert len(tapes) == 1 + assert tapes[0].measurements == [qml.expval(qml.Z(0))] + assert tapes[0].shots.total_shots == 100 + assert tapes[0].operations == [qml.X(0)] + assert qml.math.allclose(fn([0.1]), [0.1, 0.05]) + + def test_mix_measurement_types_qwc(self): + """Tests multiple measurement types can be handled by qwc grouping""" + + initial_tape = qml.tape.QuantumScript( + [qml.X(0)], + measurements=[ + # The observables in the following list of measurements are listed here. + # Note that wire-based measurements are assumed to be in the Z-basis, and + # therefore is assigned a dummy observable of Z + # [Z(0), X(0), Z(0), X(0), Y(1), Z(1), Z(0) @ Z(1), Z(1), Z(0)] + qml.expval(qml.Z(0)), + qml.expval(qml.Z(0) + qml.X(0)), + qml.var(qml.Z(0)), + qml.probs(op=qml.X(0)), + qml.counts(qml.Y(1)), + qml.sample(qml.Z(1)), + qml.probs(wires=[0, 1]), + qml.sample(wires=[1]), + qml.counts(wires=[0]), + ], ) - wires_groups = [ - [qml.X(0), qml.Z(1)], - [qml.Y(0), non_pauli_obs[0]], - [qml.X(0) @ qml.Y(1)], - [qml.Y(0) @ qml.Z(1)], - [non_pauli_obs[1]], + # The list of observables in the comment above can be placed in two groups: + # ((0, 2, 5, 6, 7, 8), (1, 3, 4)) + tapes, fn = split_non_commuting(initial_tape, grouping_strategy="qwc") + assert len(tapes) == 2 + assert tapes[0].measurements == [ + qml.expval(qml.Z(0)), + qml.var(qml.Z(0)), + qml.sample(qml.Z(1)), + qml.probs(wires=[0, 1]), + qml.sample(wires=[1]), + qml.counts(wires=[0]), + ] + assert tapes[1].measurements == [ + qml.expval(qml.X(0)), + qml.probs(op=qml.X(0)), + qml.counts(qml.Y(1)), ] + results = fn( + [ + [ + 0.1, + 0.2, + np.array([1.0, 1.0]), + np.array([1.0, 0.0, 0.0, 0.0]), + np.array([0, 0]), + {"0": 2}, + ], + [0.5, np.array([0.5, 0.5]), {"1": 2}], + ] + ) + expected = ( + 0.1, + 0.1 + 0.5, + 0.2, + np.array([0.5, 0.5]), + {"1": 2}, + np.array([1.0, 1.0]), + np.array([1.0, 0.0, 0.0, 0.0]), + np.array([0, 0]), + {"0": 2}, + ) + assert len(results) == len(expected) + for res, _expected in zip(results, expected): + if isinstance(res, np.ndarray): + assert np.allclose(res, _expected) + else: + assert res == _expected + + def test_mix_measurement_types_wires(self): + """Tests multiple measurement types can be handled by wire-based grouping""" + + initial_tape = qml.tape.QuantumScript( + [qml.X(0)], + measurements=[ + # [Z(0), X(0), Z(0), X(0), Y(1), Z(1), Z(0) @ Z(1), Z(1), Z(0)] + qml.expval(qml.Z(0)), + qml.expval(qml.Z(0) + qml.X(0)), + qml.var(qml.Z(0)), + qml.probs(op=qml.X(0)), + qml.counts(qml.Y(1)), + qml.sample(qml.Z(1)), + qml.probs(wires=[0, 1]), + qml.sample(wires=[1]), + qml.counts(wires=[0]), + ], + ) - # wires grouping produces [[0, 2], [1, 5], [3], [4], [6]] - expected_tapes_wires_grouping = [ - qml.tape.QuantumScript([], [qml.expval(o) for o in group], shots=100) - for group in wires_groups + # ((0, 4), (1, 5), (2, 7), (3, ), (6, ), (8, )) + tapes, fn = split_non_commuting(initial_tape, grouping_strategy="wires") + assert len(tapes) == 6 + expected_groups = [ + [qml.expval(qml.Z(0)), qml.counts(qml.Y(1))], + [qml.expval(qml.X(0)), qml.sample(qml.Z(1))], + [qml.var(qml.Z(0)), qml.sample(wires=[1])], + [qml.probs(op=qml.X(0))], + [qml.probs(wires=[0, 1])], + [qml.counts(wires=[0])], ] + for tape, group in zip(tapes, expected_groups): + assert tape.measurements == group - tapes, fn = split_non_commuting(tape) - for actual_tape, expected_tape in zip(tapes, expected_tapes_wires_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose( - fn([[0.1, 0.2], [0.3, 0.6], 0.4, 0.5, 0.7]), [0.01, 0.06, 0.06, 0.16, 0.25, 0.36, 0.49] + results = fn( + [ + [0.1, {"0": 2}], + [0.3, np.array([1.0, 1.0])], + [0.9, np.array([-1.0, -1.0])], + np.array([0.5, 0.5]), + np.array([1.0, 0.0, 0.0, 0.0]), + {"1": 2}, + ] + ) + expected = ( + 0.1, + 0.1 + 0.3, + 0.9, + np.array([0.5, 0.5]), + {"0": 2}, + np.array([1.0, 1.0]), + np.array([1.0, 0.0, 0.0, 0.0]), + np.array([-1.0, -1.0]), + {"1": 2}, ) - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - def test_no_measurements(self, grouping_strategy): - """Test that if the tape contains no measurements, the transform doesn't - modify it""" - - tape = qml.tape.QuantumScript([qml.X(0)]) - tapes, post_processing_fn = split_non_commuting(tape, grouping_strategy=grouping_strategy) - assert len(tapes) == 1 - assert tapes[0] == tape - assert post_processing_fn(tapes) == tape + assert len(results) == len(expected) + for res, _expected in zip(results, expected): + if isinstance(res, np.ndarray): + assert np.allclose(res, _expected) + else: + assert res == _expected - @pytest.mark.parametrize( - "observable", - [ - qml.X(0) + qml.Y(1), - 2 * (qml.X(0) + qml.Y(1)), - 3 * (2 * (qml.X(0) + qml.Y(1)) + qml.X(1)), - ], - ) - def test_splitting_sums_in_unsupported_mps_raises_error(self, observable): + @pytest.mark.parametrize("grouping_strategy", ["qwc", "wires"]) + def test_state_measurement_in_separate_tape(self, grouping_strategy): + """Tests that a state measurement is in a separate tape - tape = qml.tape.QuantumScript([qml.X(0)], measurements=[qml.counts(observable)]) - with pytest.raises( - RuntimeError, match="Cannot split up terms in sums for MeasurementProcess" - ): - _, _ = split_non_commuting(tape) + The legacy device does not support state measurements combined with any other + measurement, so each state measurement must be in its own tape. + """ -class TestIntegration: - """Tests the ``split_non_commuting`` transform performed on a QNode""" + measurements = [qml.expval(qml.Z(0)), qml.state()] + initial_tape = qml.tape.QuantumScript([qml.X(0)], measurements, shots=100) + tapes, _ = split_non_commuting(initial_tape, grouping_strategy=grouping_strategy) + assert len(tapes) == 2 + for tape, meas in zip(tapes, measurements): + assert tape.measurements == [meas] + assert tape.shots == initial_tape.shots + assert tape.operations == [qml.X(0)] - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - @pytest.mark.parametrize("shots", [None, 20000, [20000, 30000, 40000]]) @pytest.mark.parametrize( - "params, expected_results", + "obs", [ - ( - [np.pi / 4, 3 * np.pi / 4], - [ - 0.5, - -np.cos(np.pi / 4), - -0.5, - -0.5 * np.cos(np.pi / 4), - 0.5 * np.cos(np.pi / 4), - ], - ), - ( - [[np.pi / 4, 3 * np.pi / 4], [3 * np.pi / 4, 3 * np.pi / 4]], - [ - [0.5, -0.5], - [-np.cos(np.pi / 4), -np.cos(np.pi / 4)], - [-0.5, 0.5], - [-0.5 * np.cos(np.pi / 4), 0.5 * np.cos(np.pi / 4)], - [0.5 * np.cos(np.pi / 4), -0.5 * np.cos(np.pi / 4)], - ], - ), + qml.X(0) + qml.Y(1), + 2 * (qml.X(0) + qml.Y(1)), + (qml.X(0) + qml.Y(1)) @ qml.X(1), ], ) - def test_single_expval(self, grouping_strategy, shots, params, expected_results): - """Tests that a QNode with a single expval measurement is executed correctly""" - - coeffs, obs = [0.1, 0.2, 0.3, 0.4, 0.5], single_term_obs_list + def test_unsupported_mps_of_sum(self, obs): + """Tests a measurement of Sum other than expval raises an error.""" - # test constant offset - coeffs, obs = coeffs + [0.6], obs + [qml.I()] + initial_tape = qml.tape.QuantumScript([], measurements=[qml.counts(obs)]) + with pytest.raises(RuntimeError, match="Cannot split up terms in sums"): + _, __ = split_non_commuting(initial_tape) - dev = qml.device("default.qubit", wires=2, shots=shots) - @qml.qnode(dev) - def circuit(angles): - qml.RX(angles[0], wires=0) - qml.RY(angles[1], wires=0) - qml.RX(angles[0], wires=1) - qml.RY(angles[1], wires=1) - return qml.expval(qml.Hamiltonian(coeffs, obs)) - - circuit = split_non_commuting(circuit, grouping_strategy=grouping_strategy) - res = circuit(params) +@pytest.mark.system +class TestQNodeIntegration: + """Tests that split_non_commuting is correctly applied to a QNode. - identity_results = [1] if len(np.shape(params)) == 1 else [[1, 1]] - expected_results = expected_results + identity_results + This test class focuses on testing the ``split_non_commuting`` transform applied to a QNode. + These are end-to-end tests for how the transform integrates with the full execution workflow. + Here we include tests for different combinations of shot vectors and parameter broadcasting, + as well as some edge cases. - expected = np.dot(coeffs, expected_results) + It's typically unnecessary to test numerical correctness here, since the unit tests in the + test above should have caught any mathematical errors that the transform might make. Here we + simply want to make sure that the workflow executes without error, and that the results + have the correct shapes. Note that the differentiation tests in the test class below will + test the numerical correctness of the derivatives, which acts as a safeguard. - if isinstance(shots, list): - assert qml.math.shape(res) == (3,) if len(np.shape(res)) == 1 else (3, 2) - for i in range(3): - assert qml.math.allclose(res[i], expected, atol=0.05) - else: - assert qml.math.allclose(res, expected, atol=0.05) + """ - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - @pytest.mark.parametrize("shots", [None, 20000, [20000, 30000, 40000]]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) + @pytest.mark.parametrize("shots", [None, 10, [10, 20, 30]]) @pytest.mark.parametrize( - "params, expected_results", + "params", [ - ( - [np.pi / 4, 3 * np.pi / 4], - [ - 0.5, - -0.5 * np.cos(np.pi / 4), - 0.5 + np.cos(np.pi / 4) * 0.5 + 2.0 * 0.5 + 1, - np.dot( - [0.1, 0.2, 0.3, 0.4], - [-0.5, -0.5 * np.cos(np.pi / 4), 0.5 * np.cos(np.pi / 4), 1], - ), - 1.5, - ], - ), - ( - [[np.pi / 4, 3 * np.pi / 4], [3 * np.pi / 4, 3 * np.pi / 4]], - [ - [0.5, -0.5], - [-0.5 * np.cos(np.pi / 4), -0.5 * np.cos(np.pi / 4)], - [ - 0.5 + np.cos(np.pi / 4) * 0.5 + 2.0 * 0.5 + 1, - -0.5 - np.cos(np.pi / 4) * 0.5 - 2.0 * 0.5 + 1, - ], - [ - np.dot( - [0.1, 0.2, 0.3, 0.4], - [-0.5, -0.5 * np.cos(np.pi / 4), 0.5 * np.cos(np.pi / 4), 1], - ), - np.dot( - [0.1, 0.2, 0.3, 0.4], - [0.5, 0.5 * np.cos(np.pi / 4), -0.5 * np.cos(np.pi / 4), 1], - ), - ], - [1.5, 1.5], - ], - ), + [0.1, 0.2, 0.3], + [[0.1, 0.1], [0.2, 0.2], [0.3, 0.3]], ], ) - def test_multiple_expval(self, grouping_strategy, shots, params, expected_results, seed): - """Tests that a QNode with multiple expval measurements is executed correctly""" + def test_measurement_of_single_hamiltonian(self, grouping_strategy, shots, params): + """Tests executing a QNode returning a single measurement of a Hamiltonian.""" - dev = qml.device("default.qubit", wires=2, shots=shots, seed=seed) + coeffs, obs = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], single_term_obs_list - obs_list = complex_obs_list + dev = qml.device("default.qubit", wires=3, shots=shots) + @partial(split_non_commuting, grouping_strategy=grouping_strategy) @qml.qnode(dev) def circuit(angles): qml.RX(angles[0], wires=0) - qml.RY(angles[1], wires=0) - qml.RX(angles[0], wires=1) qml.RY(angles[1], wires=1) - return [qml.expval(obs) for obs in obs_list] + qml.RZ(angles[2], wires=2) + return qml.expval(qml.Hamiltonian(coeffs, obs)) - circuit = split_non_commuting(circuit, grouping_strategy=grouping_strategy) res = circuit(params) - if isinstance(shots, list): - assert qml.math.shape(res) == (3, *np.shape(expected_results)) - for i in range(3): - assert qml.math.allclose(res[i], expected_results, atol=0.05) - else: - assert qml.math.allclose(res, expected_results, atol=0.05) + shot_dimension = (3,) if isinstance(shots, list) else () + parameter_dimension = (2,) if len(qml.math.shape(params)) > 1 else () + expected_dimension = shot_dimension + parameter_dimension + assert qml.math.shape(res) == expected_dimension - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - @pytest.mark.parametrize("shots", [20000, [20000, 30000, 40000]]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) + @pytest.mark.parametrize("shots", [10, [10, 20, 30]]) @pytest.mark.parametrize( - "params, expected_results", + "params", [ - ( - [np.pi / 4, 3 * np.pi / 4], - [ - 0.5, - -0.5 * np.cos(np.pi / 4), - 0.5 + np.cos(np.pi / 4) * 0.5 + 2.0 * 0.5 + 1, - np.dot( - [0.1, 0.2, 0.3, 0.4], - [-0.5, -0.5 * np.cos(np.pi / 4), 0.5 * np.cos(np.pi / 4), 1], - ), - 1.5, - ], - ), - ( - [[np.pi / 4, 3 * np.pi / 4], [3 * np.pi / 4, 3 * np.pi / 4]], - [ - [0.5, -0.5], - [-0.5 * np.cos(np.pi / 4), -0.5 * np.cos(np.pi / 4)], - [ - 0.5 + np.cos(np.pi / 4) * 0.5 + 2.0 * 0.5 + 1, - -0.5 - np.cos(np.pi / 4) * 0.5 - 2.0 * 0.5 + 1, - ], - [ - np.dot( - [0.1, 0.2, 0.3, 0.4], - [-0.5, -0.5 * np.cos(np.pi / 4), 0.5 * np.cos(np.pi / 4), 1], - ), - np.dot( - [0.1, 0.2, 0.3, 0.4], - [0.5, 0.5 * np.cos(np.pi / 4), -0.5 * np.cos(np.pi / 4), 1], - ), - ], - [1.5, 1.5], - ], - ), + [0.1, 0.2, 0.3], + [[0.1, 0.1], [0.2, 0.2], [0.3, 0.3]], ], ) - def test_mixed_measurement_types( - self, grouping_strategy, shots, params, expected_results, seed - ): - """Tests that a QNode with mixed measurement types is executed correctly""" - - dev = qml.device("default.qubit", wires=2, shots=shots, seed=seed) + def test_general_circuits(self, grouping_strategy, shots, params): + """Tests executing a QNode with different grouping strategies on a typical circuit.""" - obs_list = complex_obs_list + dev = qml.device("default.qubit", wires=3, shots=shots) + @partial(split_non_commuting, grouping_strategy=grouping_strategy) @qml.qnode(dev) def circuit(angles): qml.RX(angles[0], wires=0) - qml.RY(angles[1], wires=0) - qml.RX(angles[0], wires=1) qml.RY(angles[1], wires=1) + qml.RZ(angles[2], wires=2) return ( - qml.probs(wires=0), + qml.expval(qml.X(0) + qml.Y(0) @ qml.Z(1) + 2.0 * qml.X(1) + qml.I()), + qml.expval(qml.X(0)), + qml.expval(0.5 * qml.Y(0)), + qml.expval(1.5 * qml.I(0)), + qml.var(qml.Z(0)), + qml.probs(op=qml.X(0)), + qml.counts(qml.Y(1)), + qml.sample(qml.Z(1)), qml.probs(wires=[0, 1]), - qml.counts(wires=0), - qml.sample(wires=0), - *[qml.expval(obs) for obs in obs_list], + qml.sample(wires=[1]), + qml.counts(wires=[0]), ) - circuit = split_non_commuting(circuit, grouping_strategy=grouping_strategy) - res = circuit(params) + shot_dimension = (3,) if isinstance(shots, list) else () + measurements_dimension = (11,) + parameter_dimension = (2,) if len(qml.math.shape(params)) > 1 else () + expected_shape = shot_dimension + measurements_dimension + parameter_dimension - if isinstance(shots, list): - assert len(res) == 3 - for i in range(3): - - prob_res_0 = res[i][0] - prob_res_1 = res[i][1] - counts_res = res[i][2] - sample_res = res[i][3] - if len(qml.math.shape(params)) == 1: - assert qml.math.shape(prob_res_0) == (2,) - assert qml.math.shape(prob_res_1) == (4,) - assert isinstance(counts_res, dict) - assert qml.math.shape(sample_res) == (shots[i],) - else: - assert qml.math.shape(prob_res_0) == (2, 2) - assert qml.math.shape(prob_res_1) == (2, 4) - assert all(isinstance(_res, dict) for _res in counts_res) - assert qml.math.shape(sample_res) == (2, shots[i]) - - expval_res = res[i][4:] - assert qml.math.allclose(expval_res, expected_results, atol=0.05) - else: - prob_res_0 = res[0] - prob_res_1 = res[1] - counts_res = res[2] - sample_res = res[3] - if len(qml.math.shape(params)) == 1: - assert qml.math.shape(prob_res_0) == (2,) - assert qml.math.shape(prob_res_1) == (4,) - assert isinstance(counts_res, dict) - assert qml.math.shape(sample_res) == (shots,) - else: - assert qml.math.shape(prob_res_0) == (2, 2) - assert qml.math.shape(prob_res_1) == (2, 4) - assert all(isinstance(_res, dict) for _res in counts_res) - assert qml.math.shape(sample_res) == (2, shots) + result = circuit(params) - expval_res = res[4:] - assert qml.math.allclose(expval_res, expected_results, atol=0.05) + def _recursively_check_shape(_result, _expected_shape): + """Recursively check the shape of _result and _expected_shape. - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - def test_single_hamiltonian_only_constant_offset(self, grouping_strategy): - """Tests that split_non_commuting can handle a single Identity observable""" + ``qml.math.shape`` will not work in this case because there are arrays of different + shapes and dictionaries nested in the results. - dev = qml.device("default.qubit", wires=2) - H = qml.Hamiltonian([1.5, 2.5], [qml.I(), qml.I()]) + """ - @partial(split_non_commuting, grouping_strategy=grouping_strategy) - @qml.qnode(dev) - def circuit(): - return qml.expval(H) - - with dev.tracker: - res = circuit() - assert dev.tracker.totals == {} - assert qml.math.allclose(res, 4.0) - - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - def test_no_obs_tape(self, grouping_strategy): - """Tests tapes with only constant offsets (only measurements on Identity)""" - - _dev = qml.device("default.qubit", wires=1) - - @qml.qnode(_dev) - def circuit(): - return qml.expval(1.5 * qml.I(0)) - - circuit = split_non_commuting(circuit, grouping_strategy=grouping_strategy) - - with _dev.tracker: - res = circuit() - - assert _dev.tracker.totals == {} - assert qml.math.allclose(res, 1.5) + if not _expected_shape: + return - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) - def test_no_obs_tape_multi_measurement(self, grouping_strategy): - """Tests tapes with only constant offsets (only measurements on Identity)""" + assert len(_result) == _expected_shape[0] + for res in _result: + _recursively_check_shape(res, _expected_shape[1:]) - _dev = qml.device("default.qubit", wires=1) + _recursively_check_shape(result, expected_shape) - @qml.qnode(_dev) - def circuit(): - return qml.expval(1.5 * qml.I()), qml.expval(2.5 * qml.I()) - - circuit = split_non_commuting(circuit, grouping_strategy=grouping_strategy) - - with _dev.tracker: - res = circuit() - - assert _dev.tracker.totals == {} - assert qml.math.allclose(res, [1.5, 2.5]) - - def test_non_pauli_obs_in_circuit(self): - """Tests that the tape is executed correctly with non-pauli observables""" + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) + @pytest.mark.parametrize( + "obs, expected_result", + [ + ([qml.Hamiltonian([1.5, 2.5], [qml.I(0), qml.I(1)])], 4.0), + ([1.5 * qml.I(), 2.5 * qml.I()], [1.5, 2.5]), + ], + ) + def test_only_constant_offset(self, grouping_strategy, obs, expected_result): + """Tests that split_non_commuting can handle a circuit only measuring Identity.""" - _dev = qml.device("default.qubit", wires=1) + dev = qml.device("default.qubit", wires=2) - @qml.transforms.split_non_commuting - @qml.qnode(_dev) + @partial(split_non_commuting, grouping_strategy=grouping_strategy) + @qml.qnode(dev) def circuit(): - qml.Hadamard(0) - return ( - qml.expval(qml.Projector([0], wires=[0])), - qml.expval(qml.Projector([1], wires=[0])), - ) + return tuple(qml.expval(ob) for ob in obs) - with _dev.tracker: + with dev.tracker: res = circuit() - assert _dev.tracker.totals["simulations"] == 2 - assert qml.math.allclose(res, [0.5, 0.5]) + assert dev.tracker.totals == {} + assert qml.math.allclose(res, expected_result) expected_grad_param_0 = [ @@ -908,28 +731,46 @@ def circuit(): ] +def circuit_to_test(device, grouping_strategy): + """The test circuit used in differentiation tests.""" + + @partial(split_non_commuting, grouping_strategy=grouping_strategy) + @qml.qnode(device) + def circuit(theta, phi): + qml.RX(theta, wires=0) + qml.RY(phi, wires=0) + qml.RX(theta, wires=1) + qml.RY(phi, wires=1) + return qml.probs(wires=[0, 1]), *[qml.expval(o) for o in complex_obs_list] + + return circuit + + +def circuit_with_trainable_H(device, grouping_strategy): + """Test circuit with trainable Hamiltonian.""" + + @partial(split_non_commuting, grouping_strategy=grouping_strategy) + @qml.qnode(device) + def circuit(coeff1, coeff2): + qml.RX(np.pi / 4, wires=0) + qml.RY(np.pi / 4, wires=1) + return qml.expval(qml.Hamiltonian([coeff1, coeff2], [qml.Y(0) @ qml.Z(1), qml.X(1)])) + + return circuit + + class TestDifferentiability: """Tests the differentiability of the ``split_non_commuting`` transform""" @pytest.mark.autograd - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_autograd(self, grouping_strategy): """Tests that the output of ``split_non_commuting`` is differentiable with autograd""" import pennylane.numpy as pnp dev = qml.device("default.qubit", wires=2) - - obs_list = complex_obs_list - - @partial(split_non_commuting, grouping_strategy=grouping_strategy) - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RY(phi, wires=0) - qml.RX(theta, wires=1) - qml.RY(phi, wires=1) - return qml.probs(wires=[0, 1]), *[qml.expval(o) for o in obs_list] + circuit = circuit_to_test(dev, grouping_strategy=grouping_strategy) def cost(theta, phi): res = circuit(theta, phi) @@ -945,37 +786,31 @@ def cost(theta, phi): assert qml.math.allclose(grad2, expected_grad_2) @pytest.mark.autograd - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_trainable_hamiltonian_autograd(self, grouping_strategy): """Tests that measurements of trainable Hamiltonians are differentiable""" import pennylane.numpy as pnp - dev = qml.device("default.qubit", wires=2, shots=50000) - - @partial(split_non_commuting, grouping_strategy=grouping_strategy) - @qml.qnode(dev) - def circuit(coeff1, coeff2): - qml.RX(np.pi / 4, wires=0) - qml.RY(np.pi / 4, wires=1) - return qml.expval(qml.Hamiltonian([coeff1, coeff2], [qml.Y(0) @ qml.Z(1), qml.X(1)])) + dev = qml.device("default.qubit", wires=2) + circuit = circuit_with_trainable_H(dev, grouping_strategy=grouping_strategy) params = pnp.array(pnp.pi / 4), pnp.array(3 * pnp.pi / 4) actual = qml.jacobian(circuit)(*params) - assert qml.math.allclose(actual, [-0.5, np.cos(np.pi / 4)], rtol=0.05) + assert qml.math.allclose(actual, [-0.5, np.cos(np.pi / 4)]) @pytest.mark.autograd - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_non_trainable_obs_autograd(self, grouping_strategy): """Test that we can measure a hamiltonian with non-trainable autograd coefficients.""" dev = qml.device("default.qubit") @partial(split_non_commuting, grouping_strategy=grouping_strategy) - @qml.qnode(dev, diff_method="adjoint") - def circuit(x): - qml.RX(x, 0) + @qml.qnode(dev) + def circuit(param): + qml.RX(param, 0) c1 = qml.numpy.array(0.1, requires_grad=False) c2 = qml.numpy.array(0.2, requires_grad=False) H = c1 * qml.Z(0) + c2 * qml.X(0) + c2 * qml.I(0) @@ -988,7 +823,7 @@ def circuit(x): @pytest.mark.jax @pytest.mark.parametrize("use_jit", [False, True]) - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_jax(self, grouping_strategy, use_jit): """Tests that the output of ``split_non_commuting`` is differentiable with jax""" @@ -996,17 +831,7 @@ def test_jax(self, grouping_strategy, use_jit): import jax.numpy as jnp dev = qml.device("default.qubit", wires=2) - - obs_list = complex_obs_list - - @partial(split_non_commuting, grouping_strategy=grouping_strategy) - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RY(phi, wires=0) - qml.RX(theta, wires=1) - qml.RY(phi, wires=1) - return qml.probs(wires=[0, 1]), *[qml.expval(o) for o in obs_list] + circuit = circuit_to_test(dev, grouping_strategy=grouping_strategy) if use_jit: circuit = jax.jit(circuit) @@ -1026,21 +851,15 @@ def cost(theta, phi): @pytest.mark.jax @pytest.mark.parametrize("use_jit", [False, True]) - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_trainable_hamiltonian_jax(self, grouping_strategy, use_jit): """Tests that measurements of trainable Hamiltonians are differentiable with jax""" import jax import jax.numpy as jnp - dev = qml.device("default.qubit", wires=2, shots=50000) - - @partial(split_non_commuting, grouping_strategy=grouping_strategy) - @qml.qnode(dev) - def circuit(coeff1, coeff2): - qml.RX(np.pi / 4, wires=0) - qml.RY(np.pi / 4, wires=1) - return qml.expval(qml.Hamiltonian([coeff1, coeff2], [qml.Y(0) @ qml.Z(1), qml.X(1)])) + dev = qml.device("default.qubit", wires=2) + circuit = circuit_with_trainable_H(dev, grouping_strategy=grouping_strategy) if use_jit: circuit = jax.jit(circuit) @@ -1048,10 +867,10 @@ def circuit(coeff1, coeff2): params = jnp.array(np.pi / 4), jnp.array(3 * np.pi / 4) actual = jax.jacobian(circuit, argnums=[0, 1])(*params) - assert qml.math.allclose(actual, [-0.5, np.cos(np.pi / 4)], rtol=0.05) + assert qml.math.allclose(actual, [-0.5, np.cos(np.pi / 4)]) @pytest.mark.torch - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_torch(self, grouping_strategy): """Tests that the output of ``split_non_commuting`` is differentiable with torch""" @@ -1059,17 +878,7 @@ def test_torch(self, grouping_strategy): from torch.autograd.functional import jacobian dev = qml.device("default.qubit", wires=2) - - obs_list = complex_obs_list - - @partial(split_non_commuting, grouping_strategy=grouping_strategy) - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RY(phi, wires=0) - qml.RX(theta, wires=1) - qml.RY(phi, wires=1) - return qml.probs(wires=[0, 1]), *[qml.expval(o) for o in obs_list] + circuit = circuit_to_test(dev, grouping_strategy=grouping_strategy) def cost(theta, phi): res = circuit(theta, phi) @@ -1081,54 +890,39 @@ def cost(theta, phi): expected_grad_1 = expected_grad_param_0 expected_grad_2 = expected_grad_param_1 - assert qml.math.allclose(grad1, expected_grad_1, atol=1e-5) - assert qml.math.allclose(grad2, expected_grad_2, atol=1e-5) + assert qml.math.allclose(grad1, expected_grad_1, atol=1e-7) + assert qml.math.allclose(grad2, expected_grad_2, atol=1e-7) @pytest.mark.torch - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_trainable_hamiltonian_torch(self, grouping_strategy): """Tests that measurements of trainable Hamiltonians are differentiable with torch""" import torch from torch.autograd.functional import jacobian - dev = qml.device("default.qubit", wires=2, shots=50000) - - @partial(split_non_commuting, grouping_strategy=grouping_strategy) - @qml.qnode(dev) - def circuit(coeff1, coeff2): - qml.RX(np.pi / 4, wires=0) - qml.RY(np.pi / 4, wires=1) - return qml.expval(qml.Hamiltonian([coeff1, coeff2], [qml.Y(0) @ qml.Z(1), qml.X(1)])) + dev = qml.device("default.qubit", wires=2) + circuit = circuit_with_trainable_H(dev, grouping_strategy=grouping_strategy) params = torch.tensor(np.pi / 4), torch.tensor(3 * np.pi / 4) actual = jacobian(circuit, params) - assert qml.math.allclose(actual, [-0.5, np.cos(np.pi / 4)], rtol=0.05) + assert qml.math.allclose(actual, [-0.5, np.cos(np.pi / 4)]) @pytest.mark.tf - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_tensorflow(self, grouping_strategy): """Tests that the output of ``split_non_commuting`` is differentiable with tensorflow""" import tensorflow as tf dev = qml.device("default.qubit", wires=2) - - obs_list = complex_obs_list - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RY(phi, wires=0) - qml.RX(theta, wires=1) - qml.RY(phi, wires=1) - return qml.probs(wires=[0, 1]), *[qml.expval(o) for o in obs_list] + circuit = circuit_to_test(dev, grouping_strategy=grouping_strategy) params = tf.Variable(np.pi / 4), tf.Variable(3 * np.pi / 4) with tf.GradientTape() as tape: - res = split_non_commuting(circuit, grouping_strategy=grouping_strategy)(*params) + res = circuit(*params) cost = qml.math.concatenate([res[0], qml.math.stack(res[1:])], axis=0) grad1, grad2 = tape.jacobian(cost, params) @@ -1136,23 +930,18 @@ def circuit(theta, phi): expected_grad_1 = expected_grad_param_0 expected_grad_2 = expected_grad_param_1 - assert qml.math.allclose(grad1, expected_grad_1, atol=1e-5) - assert qml.math.allclose(grad2, expected_grad_2, atol=1e-5) + assert qml.math.allclose(grad1, expected_grad_1, atol=1e-7) + assert qml.math.allclose(grad2, expected_grad_2, atol=1e-7) @pytest.mark.tf - @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) + @pytest.mark.parametrize("grouping_strategy", [None, "qwc", "wires"]) def test_trainable_hamiltonian_tensorflow(self, grouping_strategy): """Tests that measurements of trainable Hamiltonians are differentiable with tensorflow""" import tensorflow as tf - dev = qml.device("default.qubit", wires=2, shots=50000) - - @qml.qnode(dev) - def circuit(coeff1, coeff2): - qml.RX(np.pi / 4, wires=0) - qml.RY(np.pi / 4, wires=1) - return qml.expval(qml.Hamiltonian([coeff1, coeff2], [qml.Y(0) @ qml.Z(1), qml.X(1)])) + dev = qml.device("default.qubit", wires=2) + circuit = circuit_with_trainable_H(dev, grouping_strategy=grouping_strategy) params = tf.Variable(np.pi / 4), tf.Variable(3 * np.pi / 4) @@ -1161,4 +950,4 @@ def circuit(coeff1, coeff2): actual = tape.jacobian(cost, params) - assert qml.math.allclose(actual, [-0.5, np.cos(np.pi / 4)], rtol=0.05) + assert qml.math.allclose(actual, [-0.5, np.cos(np.pi / 4)])