diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index db929d3a5c4..a500b27c012 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -3,6 +3,7 @@ # Release 0.38.0-dev (development release)

New features since last release

+ * A new method `process_density_matrix` has been added to the `ProbabilityMP` and `DensityMatrixMP` classes, allowing for more efficient handling of quantum density matrices, particularly with batch processing support. This method simplifies the calculation of probabilities from quantum states represented as density matrices. [(#5830)](https://github.com/PennyLaneAI/pennylane/pull/5830) @@ -23,6 +24,9 @@ * `QuantumScript.hash` is now cached, leading to performance improvements. [(#5919)](https://github.com/PennyLaneAI/pennylane/pull/5919) +* Observable validation for `default.qubit` is now based on execution mode (analytic vs. finite shots) and measurement type (sample measurement vs. state measurement). + [(#5890)](https://github.com/PennyLaneAI/pennylane/pull/5890) +

Breaking changes 💔

* `QuantumScript.interface` has been removed. @@ -43,6 +47,8 @@

Contributors ✍️

This release contains contributions from (in alphabetical order): + +Astral Cai, Yushao Chen, Christina Lee, William Maxwell, diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 865bedf6b85..aebbe4bbeeb 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -60,30 +60,6 @@ PostprocessingFn = Callable[[ResultBatch], Result_or_ResultBatch] -observables = { - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Identity", - "Projector", - "SparseHamiltonian", - "Hamiltonian", - "LinearCombination", - "Sum", - "SProd", - "Prod", - "Exp", - "Evolution", -} - - -def observable_stopping_condition(obs: qml.operation.Operator) -> bool: - """Specifies whether or not an observable is accepted by DefaultQubit.""" - return obs.name in observables - - def stopping_condition(op: qml.operation.Operator) -> bool: """Specify whether or not an Operator object is supported by the device.""" if op.name == "QFT" and len(op.wires) >= 6: @@ -103,16 +79,74 @@ def stopping_condition_shots(op: qml.operation.Operator) -> bool: return isinstance(op, (Conditional, MidMeasureMP)) or stopping_condition(op) +def observable_accepts_sampling(obs: qml.operation.Operator) -> bool: + """Verifies whether an observable supports sample measurement""" + + if isinstance(obs, qml.ops.CompositeOp): + return all(observable_accepts_sampling(o) for o in obs.operands) + + if isinstance(obs, qml.ops.SymbolicOp): + return observable_accepts_sampling(obs.base) + + if isinstance(obs, qml.ops.Hamiltonian): + return all(observable_accepts_sampling(o) for o in obs.ops) + + if isinstance(obs, qml.operation.Tensor): + return all(observable_accepts_sampling(o) for o in obs.obs) + + return obs.has_diagonalizing_gates + + +def observable_accepts_analytic(obs: qml.operation.Operator, is_expval=False) -> bool: + """Verifies whether an observable supports analytic measurement""" + + if isinstance(obs, qml.ops.CompositeOp): + return all(observable_accepts_analytic(o, is_expval) for o in obs.operands) + + if isinstance(obs, qml.ops.SymbolicOp): + return observable_accepts_analytic(obs.base, is_expval) + + if isinstance(obs, qml.ops.Hamiltonian): + return all(observable_accepts_analytic(o, is_expval) for o in obs.ops) + + if isinstance(obs, qml.operation.Tensor): + return all(observable_accepts_analytic(o, is_expval) for o in obs.obs) + + if is_expval and isinstance(obs, (qml.ops.SparseHamiltonian, qml.ops.Hermitian)): + return True + + return obs.has_diagonalizing_gates + + def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: - """Specifies whether or not a measurement is accepted when sampling.""" - return isinstance( + """Specifies whether a measurement is accepted when sampling.""" + + if not isinstance( m, ( qml.measurements.SampleMeasurement, qml.measurements.ClassicalShadowMP, qml.measurements.ShadowExpvalMP, ), - ) + ): + return False + + if m.obs is not None: + return observable_accepts_sampling(m.obs) + + return True + + +def accepted_analytic_measurement(m: qml.measurements.MeasurementProcess) -> bool: + """Specifies whether a measurement is accepted when analytic.""" + + if not isinstance(m, qml.measurements.StateMeasurement): + return False + + if m.obs is not None: + return observable_accepts_analytic(m.obs, isinstance(m, qml.measurements.ExpectationMP)) + + return True def null_postprocessing(results): @@ -514,10 +548,10 @@ def preprocess( name=self.name, ) transform_program.add_transform( - validate_measurements, sample_measurements=accepted_sample_measurement, name=self.name - ) - transform_program.add_transform( - validate_observables, stopping_condition=observable_stopping_condition, name=self.name + validate_measurements, + analytic_measurements=accepted_analytic_measurement, + sample_measurements=accepted_sample_measurement, + name=self.name, ) if config.mcm_config.mcm_method == "tree-traversal": transform_program.add_transform(qml.transforms.broadcast_expand) diff --git a/pennylane/devices/preprocess.py b/pennylane/devices/preprocess.py index 3704d8c687b..2f38f1f9a54 100644 --- a/pennylane/devices/preprocess.py +++ b/pennylane/devices/preprocess.py @@ -479,7 +479,9 @@ def sample_measurements(m): and not isinstance(meas := op.hyperparameters["measurement"], qml.measurements.StateMP) ] - if tape.shots: + shots = qml.measurements.Shots(tape.shots) + + if shots.total_shots is not None: for m in chain(snapshot_measurements, tape.measurements): if not sample_measurements(m): raise DeviceError(f"Measurement {m} not accepted with finite shots on {name}") diff --git a/tests/devices/default_qubit/test_default_qubit_preprocessing.py b/tests/devices/default_qubit/test_default_qubit_preprocessing.py index e8610d922e0..cb1e8707008 100644 --- a/tests/devices/default_qubit/test_default_qubit_preprocessing.py +++ b/tests/devices/default_qubit/test_default_qubit_preprocessing.py @@ -20,6 +20,7 @@ from pennylane import numpy as pnp from pennylane.devices import DefaultQubit, ExecutionConfig from pennylane.devices.default_qubit import stopping_condition +from pennylane.operation import classproperty class NoMatOp(qml.operation.Operation): @@ -45,6 +46,16 @@ def has_matrix(self): return False +# pylint: disable=too-few-public-methods +class HasDiagonalizingGatesOp(qml.operation.Operator): + """Dummy observable that has diagonalizing gates.""" + + # pylint: disable=arguments-renamed,invalid-overridden-method,no-self-argument + @classproperty + def has_diagonalizing_gates(cls): + return True + + def test_snapshot_multiprocessing_execute(): """DefaultQubit cannot execute tapes with Snapshot if `max_workers` is not `None`""" dev = qml.device("default.qubit", max_workers=2) @@ -288,6 +299,58 @@ def has_matrix(self): batch, _ = program((tape4,)) assert batch[0].circuit == tape4.circuit + @pytest.mark.parametrize( + "shots, measurements, supported", + [ + # Supported measurements in analytic mode + (None, [qml.state()], True), + (None, [qml.expval(qml.X(0))], True), + (None, [qml.expval(qml.RX(0.123, 0))], False), + (None, [qml.expval(qml.SparseHamiltonian(qml.X.compute_sparse_matrix(), 0))], True), + (None, [qml.expval(qml.Hermitian(np.diag([1, 2]), wires=0))], True), + (None, [qml.var(qml.SparseHamiltonian(qml.X.compute_sparse_matrix(), 0))], False), + (None, [qml.expval(qml.X(0) @ qml.Hermitian(np.diag([1, 2]), wires=1))], True), + ( + None, + [ + qml.expval( + qml.Hamiltonian( + [0.1, 0.2], + [qml.Z(0), qml.SparseHamiltonian(qml.X.compute_sparse_matrix(), 1)], + ) + ) + ], + True, + ), + (None, [qml.expval(qml.Hamiltonian([0.1, 0.2], [qml.RZ(0.234, 0), qml.X(0)]))], False), + ( + None, + [qml.expval(qml.Hamiltonian([1, 1], [qml.Z(0), HasDiagonalizingGatesOp(1)]))], + True, + ), + # Supported measurements in finite shots mode + (100, [qml.state()], False), + (100, [qml.expval(qml.X(0))], True), + (100, [qml.expval(qml.RX(0.123, 0))], False), + (100, [qml.expval(qml.X(0) @ qml.RX(0.123, 1))], False), + (100, [qml.expval(qml.X(0) @ qml.Y(1))], True), + (100, [qml.expval(qml.Hamiltonian([0.1, 0.2], [qml.Z(0), qml.X(1)]))], True), + (100, [qml.expval(qml.Hamiltonian([0.1, 0.2], [qml.RZ(0.123, 0), qml.X(1)]))], False), + ], + ) + def test_validate_measurements(self, shots, measurements, supported): + """Tests that preprocess correctly validates measurements.""" + + device = qml.device("default.qubit") + tape = qml.tape.QuantumScript(measurements=measurements, shots=shots) + program, _ = device.preprocess() + + if not supported: + with pytest.raises(qml.DeviceError): + program([tape]) + else: + program([tape]) + class TestPreprocessingIntegration: """Test preprocess produces output that can be executed by the device.""" diff --git a/tests/devices/qubit/test_measure.py b/tests/devices/qubit/test_measure.py index 332c5971a34..d0c618311cf 100644 --- a/tests/devices/qubit/test_measure.py +++ b/tests/devices/qubit/test_measure.py @@ -179,12 +179,9 @@ def test_op_math_observable_jit_compatible(self): dev = qml.device("default.qubit", wires=4) - O1 = qml.X(0) - O2 = qml.X(0) - @qml.qnode(dev, interface="jax") def qnode(t1, t2): - return qml.expval(qml.prod(O1, qml.RX(t1, 0), O2, qml.RX(t2, 0))) + return qml.expval((t1 * qml.X(0)) @ (t2 * qml.Y(1))) t1, t2 = 0.5, 1.0 assert qml.math.allclose(qnode(t1, t2), jax.jit(qnode)(t1, t2)) diff --git a/tests/ops/op_math/test_prod.py b/tests/ops/op_math/test_prod.py index 80286e7d554..13302dc3e8d 100644 --- a/tests/ops/op_math/test_prod.py +++ b/tests/ops/op_math/test_prod.py @@ -1522,7 +1522,7 @@ def my_circ(): qml.PauliX(0) return qml.expval(prod_op) - with pytest.raises(NotImplementedError): + with pytest.raises(qml.DeviceError): my_circ() def test_operation_integration(self): diff --git a/tests/transforms/core/test_transform_dispatcher.py b/tests/transforms/core/test_transform_dispatcher.py index be9a1611c18..94f7d74e7f5 100644 --- a/tests/transforms/core/test_transform_dispatcher.py +++ b/tests/transforms/core/test_transform_dispatcher.py @@ -639,8 +639,8 @@ def test_device_transform(self, valid_transform): assert isinstance(program, qml.transforms.core.TransformProgram) assert isinstance(new_program, qml.transforms.core.TransformProgram) - assert len(program) == 5 - assert len(new_program) == 6 + assert len(program) == 4 + assert len(new_program) == 5 assert new_program[-1].transform is valid_transform diff --git a/tests/workflow/test_construct_batch.py b/tests/workflow/test_construct_batch.py index 3bf0a617ca1..8e8400b2c52 100644 --- a/tests/workflow/test_construct_batch.py +++ b/tests/workflow/test_construct_batch.py @@ -105,7 +105,7 @@ def circuit(): p_none = get_transform_program(circuit, None) assert p_dev == p_default assert p_none == p_dev - assert len(p_dev) == 9 + assert len(p_dev) == 8 config = qml.devices.ExecutionConfig(interface=getattr(circuit, "interface", None)) assert p_dev == p_grad + dev.preprocess(config)[0] @@ -140,7 +140,7 @@ def circuit(x): return qml.expval(qml.PauliZ(0)) full_prog = get_transform_program(circuit) - assert len(full_prog) == 13 + assert len(full_prog) == 12 config = qml.devices.ExecutionConfig( interface=getattr(circuit, "interface", None),