diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index eba6d01e282..7ded3b48692 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -48,6 +48,7 @@ are added to the device API for devices that provides a TOML configuration file and thus have a `capabilities` property. [(#6632)](https://github.com/PennyLaneAI/pennylane/pull/6632) + [(#6653)](https://github.com/PennyLaneAI/pennylane/pull/6653)

New `labs` module `dla` for handling dynamical Lie algebras (DLAs)

@@ -154,6 +155,11 @@ featuring a `simulate` function for simulating mixed states in analytic mode. * Added functions and dunder methods to add and multiply Resources objects in series and in parallel. [(#6567)](https://github.com/PennyLaneAI/pennylane/pull/6567) +* The `diagonalize_measurements` transform no longer raises an error for unknown observables. Instead, + they are left undiagonalized, with the expectation that observable validation will catch any undiagonalized + observables that are also unsupported by the device. + [(#6653)](https://github.com/PennyLaneAI/pennylane/pull/6653) +

Capturing and representing hybrid programs

* PennyLane transforms can now be captured as primitives with experimental program capture enabled. diff --git a/pennylane/devices/device_api.py b/pennylane/devices/device_api.py index 28bcb40df79..3969a3f011f 100644 --- a/pennylane/devices/device_api.py +++ b/pennylane/devices/device_api.py @@ -514,12 +514,30 @@ def execute_fn(tapes): capabilities_analytic = self.capabilities.filter(finite_shots=False) capabilities_shots = self.capabilities.filter(finite_shots=True) - program.add_transform( - decompose, - stopping_condition=capabilities_analytic.supports_operation, - stopping_condition_shots=capabilities_shots.supports_operation, - name=self.name, - ) + needs_diagonalization = False + base_obs = {"PauliZ": qml.Z, "PauliX": qml.X, "PauliY": qml.Y, "Hadamard": qml.H} + if ( + not all(obs in self.capabilities.observables for obs in base_obs) + # This check is to confirm that `split_non_commuting` has been applied, since + # `diagonalize_measurements` does not work with non-commuting measurements. If + # a device is flexible enough to support non-commuting observables but for some + # reason does not support all of `PauliZ`, `PauliX`, `PauliY`, and `Hadamard`, + # we consider it enough of an edge case that the device should just implement + # its own preprocessing transform. + and not self.capabilities.non_commuting_observables + ): + needs_diagonalization = True + else: + # If the circuit does not need diagonalization, we decompose the circuit before + # potentially applying `split_non_commuting` that produces multiple tapes with + # duplicated operations. Otherwise, `decompose` has to be applied last because + # `diagonalize_measurements` may add additional gates that are not supported. + program.add_transform( + decompose, + stopping_condition=capabilities_analytic.supports_operation, + stopping_condition_shots=capabilities_shots.supports_operation, + name=self.name, + ) if not self.capabilities.overlapping_observables: program.add_transform(qml.transforms.split_non_commuting, grouping_strategy="wires") @@ -528,8 +546,16 @@ def execute_fn(tapes): elif not self.capabilities.supports_observable("Sum"): program.add_transform(qml.transforms.split_to_single_terms) - # TODO: diagonalization should be part of the default transform program, but we decided - # not to include it in this PR due to complications. See sc-79422 + if needs_diagonalization: + obs_names = base_obs.keys() & self.capabilities.observables.keys() + obs = {base_obs[obs] for obs in obs_names} + program.add_transform(qml.transforms.diagonalize_measurements, supported_base_obs=obs) + program.add_transform( + decompose, + stopping_condition=lambda o: capabilities_analytic.supports_operation(o.name), + stopping_condition_shots=lambda o: capabilities_shots.supports_operation(o.name), + name=self.name, + ) program.add_transform(qml.transforms.broadcast_expand) diff --git a/pennylane/transforms/diagonalize_measurements.py b/pennylane/transforms/diagonalize_measurements.py index ba424efb4da..1203b06040f 100644 --- a/pennylane/transforms/diagonalize_measurements.py +++ b/pennylane/transforms/diagonalize_measurements.py @@ -25,7 +25,7 @@ ) from pennylane.transforms.core import transform -# pylint: disable=protected-access +# pylint: disable=protected-access,unused-argument _default_supported_obs = (qml.Z, qml.Identity) @@ -56,10 +56,17 @@ def diagonalize_measurements(tape, supported_base_obs=_default_supported_obs, to qnode (QNode) or tuple[List[QuantumScript], function]: The transformed circuit as described in :func:`qml.transform `. .. note:: - This transform will raise an error if it encounters non-commuting terms. To avoid non-commuting terms in - circuit measurements, the :func:`split_non_commuting ` transform - can be applied. - + An error will be raised if non-commuting terms are encountered. To avoid non-commuting + terms in circuit measurements, the :func:`split_non_commuting ` + transform can be applied. + + This transform will diagonalize what it can, i.e., ``qml.X``, ``qml.Y``, ``qml.Z``, + ``qml.Hadamard``, ``qml.Identity``, or a linear combination of them. Any unrecognized + observable will not raise an error, deferring to the device's validation for supported + measurements later on. Lastly, if ``diagonalize_measurements`` produces additional gates + that the device does not support, the :func:`~pennylane.devices.preprocess.decompose` + transform should be applied to ensure that the additional gates are decomposed to those + that the device supports. **Examples:** @@ -100,8 +107,21 @@ def circuit(x): .. details:: :title: Usage Details - The transform diagonalizes observables from the local Pauli basis only, i.e. it diagonalizes X, Y, Z, - and Hadamard. + The transform diagonalizes observables from the local Pauli basis only, i.e. it diagonalizes + X, Y, Z, and Hadamard. Any other observable will be unaffected: + + .. code-block:: python3 + + measurements = [ + qml.expval(qml.X(0) + qml.Hermitian([[1, 0], [0, 1]], wires=[1])) + ] + tape = qml.tape.QuantumScript(measurements=measurements) + tapes, processsing_fn = diagnalize_measurements(tape) + + >>> tapes[0].operations + [H(0)] + >>> tapes[0].measurements + [expval(Z(0) + Hermitian(array([[1, 0], [0, 1]]), wires=[1]))] The transform can also diagonalize only a subset of these operators. By default, the only supported base observable is Z. What if a backend device can handle @@ -280,9 +300,7 @@ def _change_obs_to_Z(observable): @_change_obs_to_Z.register def _change_symbolic_op(observable: SymbolicOp): - diagonalizing_gates, [new_base] = diagonalize_qwc_pauli_words( - [observable.base], - ) + diagonalizing_gates, [new_base] = diagonalize_qwc_pauli_words([observable.base]) params, hyperparams = observable.parameters, observable.hyperparameters hyperparams = copy(hyperparams) @@ -297,9 +315,7 @@ def _change_symbolic_op(observable: SymbolicOp): def _change_linear_combination(observable: LinearCombination): coeffs, obs = observable.terms() - diagonalizing_gates, new_operands = diagonalize_qwc_pauli_words( - obs, - ) + diagonalizing_gates, new_operands = diagonalize_qwc_pauli_words(obs) new_observable = LinearCombination(coeffs, new_operands) @@ -308,9 +324,7 @@ def _change_linear_combination(observable: LinearCombination): @_change_obs_to_Z.register def _change_composite_op(observable: CompositeOp): - diagonalizing_gates, new_operands = diagonalize_qwc_pauli_words( - observable.operands, - ) + diagonalizing_gates, new_operands = diagonalize_qwc_pauli_words(observable.operands) new_observable = observable.__class__(*new_operands) @@ -385,7 +399,7 @@ def _diagonalize_observable( _visited_obs = (set(), set()) if not isinstance(observable, (qml.X, qml.Y, qml.Z, qml.Hadamard, qml.Identity)): - return _diagonalize_compound_observable( + return _diagonalize_non_basic_observable( observable, _visited_obs, supported_base_obs=supported_base_obs ) @@ -419,19 +433,23 @@ def _get_obs_and_gates(obs_list, _visited_obs, supported_base_obs=_default_suppo @singledispatch -def _diagonalize_compound_observable( +def _diagonalize_non_basic_observable( observable, _visited_obs, supported_base_obs=_default_supported_obs ): - """Takes an observable consisting of multiple other observables, and changes all + """Takes an observable other than X, Y, Z, H, and I, and diagonalize it. + + For composite observables consisting of multiple other observables, it changes all unsupported obs to the measurement basis. Applies diagonalizing gates if changing - the basis of an observable whose diagonalizing gates have not already been applied.""" + the basis of an observable whose diagonalizing gates have not already been applied. + For other observables, simply skips and returns the observable as is. - raise NotImplementedError( - f"Unable to convert observable of type {type(observable)} to the measurement basis" - ) + """ + _visited_obs[0].add(observable) + _visited_obs[1].add(observable.wires[0]) + return [], observable, _visited_obs -@_diagonalize_compound_observable.register +@_diagonalize_non_basic_observable.register def _diagonalize_symbolic_op( observable: SymbolicOp, _visited_obs, supported_base_obs=_default_supported_obs ): @@ -448,7 +466,7 @@ def _diagonalize_symbolic_op( return diagonalizing_gates, new_observable, _visited_obs -@_diagonalize_compound_observable.register +@_diagonalize_non_basic_observable.register def _diagonalize_linear_combination( observable: LinearCombination, _visited_obs, supported_base_obs=_default_supported_obs ): @@ -464,7 +482,7 @@ def _diagonalize_linear_combination( return diagonalizing_gates, new_observable, _visited_obs -@_diagonalize_compound_observable.register +@_diagonalize_non_basic_observable.register def _diagonalize_composite_op( observable: CompositeOp, _visited_obs, supported_base_obs=_default_supported_obs ): diff --git a/tests/devices/test_device_api.py b/tests/devices/test_device_api.py index 72b7af73ba0..5194304d845 100644 --- a/tests/devices/test_device_api.py +++ b/tests/devices/test_device_api.py @@ -471,8 +471,10 @@ def execute( with pytest.raises(qml.DeviceError, match=r"Measurement var\(Z\(0\)\) not accepted"): _, __ = program((invalid_tape,)) - invalid_tape = QuantumScript([], [qml.expval(qml.PauliX(0))], shots=shots) - with pytest.raises(qml.DeviceError, match=r"Observable X\(0\) not supported"): + invalid_tape = QuantumScript( + [], [qml.expval(qml.Hermitian([[1.0, 0], [0, 1.0]], 0))], shots=shots + ) + with pytest.raises(qml.DeviceError, match=r"Observable Hermitian"): _, __ = program((invalid_tape,)) shots_only_meas_tape = QuantumScript([], [qml.counts()], shots=shots) @@ -555,6 +557,58 @@ def execute(self, circuits, execution_config=DefaultExecutionConfig): assert qml.transforms.split_to_single_terms not in program assert qml.transforms.split_to_single_terms not in program + @pytest.mark.usefixtures("create_temporary_toml_file") + @pytest.mark.parametrize("create_temporary_toml_file", [EXAMPLE_TOML_FILE], indirect=True) + @pytest.mark.parametrize("non_commuting_obs", [True, False]) + @pytest.mark.parametrize("all_obs_support", [True, False]) + def test_diagonalize_measurements(self, request, non_commuting_obs, all_obs_support): + """Tests that the diagonalize_measurements transform is applied correctly.""" + + class CustomDevice(Device): + + config_filepath = request.node.toml_file + + def __init__(self): + super().__init__() + self.capabilities.non_commuting_observables = non_commuting_obs + if all_obs_support: + self.capabilities.observables.update( + { + "PauliX": OperatorProperties(), + "PauliY": OperatorProperties(), + "PauliZ": OperatorProperties(), + "Hadamard": OperatorProperties(), + } + ) + else: + self.capabilities.observables.update( + { + "PauliZ": OperatorProperties(), + "PauliX": OperatorProperties(), + "PauliY": OperatorProperties(), + "Hermitian": OperatorProperties(), + } + ) + + def execute(self, circuits, execution_config=DefaultExecutionConfig): + return (0,) + + dev = CustomDevice() + program = dev.preprocess_transforms() + if non_commuting_obs is True: + assert qml.transforms.diagonalize_measurements not in program + elif all_obs_support is True: + assert qml.transforms.diagonalize_measurements not in program + else: + assert qml.transforms.diagonalize_measurements in program + for transform_container in program: + if transform_container._transform is qml.transforms.diagonalize_measurements: + assert transform_container._kwargs["supported_base_obs"] == { + "PauliZ", + "PauliX", + "PauliY", + } + class TestMinimalDevice: """Tests for a device with only a minimal execute provided.""" diff --git a/tests/measurements/test_probs.py b/tests/measurements/test_probs.py index 9f6114a0c80..a979620cb95 100644 --- a/tests/measurements/test_probs.py +++ b/tests/measurements/test_probs.py @@ -405,11 +405,11 @@ def circuit(phi): @pytest.mark.parametrize("shots", [None, 1111, [1111, 1111]]) @pytest.mark.parametrize("phi", [0.0, np.pi / 3, np.pi]) def test_observable_is_measurement_value_list( - self, shots, phi, tol, tol_stochastic + self, shots, phi, tol, tol_stochastic, seed ): # 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") + dev = qml.device("default.qubit", seed=seed) @qml.qnode(dev) def circuit(phi): diff --git a/tests/transforms/test_diagonalize_measurements.py b/tests/transforms/test_diagonalize_measurements.py index 7cf9efc4349..beba8b7af5f 100644 --- a/tests/transforms/test_diagonalize_measurements.py +++ b/tests/transforms/test_diagonalize_measurements.py @@ -215,8 +215,8 @@ def test_non_commuting_measurements_with_supported_obs(self, obs): with pytest.raises(ValueError, match="Expected measurements on the same wire to commute"): _ = _diagonalize_observable(obs, supported_base_obs=device_supported_obs) - def test_diagonalizing_unknown_observable_raises_error(self): - """Test that an unknown observable raises an error when diagonalizing""" + def test_diagonalizing_unknown_observable(self): + """Test that an unknown observable is left undiagonalized""" # pylint: disable=too-few-public-methods class MyObs(qml.operation.Observable): @@ -225,8 +225,10 @@ class MyObs(qml.operation.Observable): def name(self): return f"MyObservable[{self.wires}]" - with pytest.raises(NotImplementedError, match="Unable to convert observable"): - _ = _diagonalize_observable(MyObs(wires=[2])) + initial_tape = qml.tape.QuantumScript([], [qml.expval(MyObs(wires=[2]))]) + tapes, _ = diagonalize_measurements([initial_tape]) + assert tapes[0].operations == [] + assert tapes[0].measurements == [ExpectationMP(MyObs(wires=[2]))] @pytest.mark.parametrize( "obs, input_visited_obs, switch_basis, expected_res",