From a63726b1053e32099aae35f6a641c14ad8688da2 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 20 Jun 2024 15:49:17 -0400 Subject: [PATCH 01/10] More sophisiticated measurement validation for default-qubit --- pennylane/devices/default_qubit.py | 84 +++++++++++++++++++----------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 865bedf6b85..19c7b9b576b 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,62 @@ 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.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 obs.has_diagonalizing_gates(): + return True + + if is_expval: + return isinstance(obs, (qml.ops.SparseHamiltonian, qml.ops.Hermitian)) + + return True + + 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 +536,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) From 2811946a95f80bca71558721869e349e08eead08 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 20 Jun 2024 16:01:56 -0400 Subject: [PATCH 02/10] fix bug --- pennylane/devices/default_qubit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 19c7b9b576b..7c9d581f48e 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -88,7 +88,7 @@ def observable_accepts_sampling(obs: qml.operation.Operator) -> bool: if isinstance(obs, qml.operation.Tensor): return all(observable_accepts_sampling(o) for o in obs.obs) - return obs.has_diagonalizing_gates() + return obs.has_diagonalizing_gates def observable_accepts_analytic(obs: qml.operation.Operator, is_expval=False) -> bool: @@ -97,7 +97,7 @@ def observable_accepts_analytic(obs: qml.operation.Operator, is_expval=False) -> if isinstance(obs, qml.ops.CompositeOp): return all(observable_accepts_analytic(o, is_expval) for o in obs.operands) - if obs.has_diagonalizing_gates(): + if obs.has_diagonalizing_gates: return True if is_expval: From e053026805e56d75c436af134a3f642ad396a15c Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 20 Jun 2024 16:35:09 -0400 Subject: [PATCH 03/10] bug fix for legacy opmath --- pennylane/devices/default_qubit.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 7c9d581f48e..4079722048b 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -85,6 +85,9 @@ def observable_accepts_sampling(obs: qml.operation.Operator) -> bool: if isinstance(obs, qml.ops.CompositeOp): return all(observable_accepts_sampling(o) for o in obs.operands) + 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) @@ -97,6 +100,12 @@ def observable_accepts_analytic(obs: qml.operation.Operator, is_expval=False) -> if isinstance(obs, qml.ops.CompositeOp): return all(observable_accepts_analytic(o, is_expval) for o in obs.operands) + 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 obs.has_diagonalizing_gates: return True From 44e0e710627e008032aebc2c5793a387e7ec3715 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Fri, 21 Jun 2024 09:39:35 -0400 Subject: [PATCH 04/10] test updates --- tests/ops/op_math/test_prod.py | 2 +- tests/transforms/core/test_transform_dispatcher.py | 4 ++-- tests/workflow/test_construct_batch.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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..6892065bc72 100644 --- a/tests/workflow/test_construct_batch.py +++ b/tests/workflow/test_construct_batch.py @@ -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), From 7d79db8fe9b21795bf1dc684bcc5d732a3be7426 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Fri, 21 Jun 2024 10:20:24 -0400 Subject: [PATCH 05/10] Fix more tests --- tests/devices/qubit/test_measure.py | 6 ++++-- tests/workflow/test_construct_batch.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/devices/qubit/test_measure.py b/tests/devices/qubit/test_measure.py index 332c5971a34..e7426f7f437 100644 --- a/tests/devices/qubit/test_measure.py +++ b/tests/devices/qubit/test_measure.py @@ -180,11 +180,13 @@ def test_op_math_observable_jit_compatible(self): dev = qml.device("default.qubit", wires=4) O1 = qml.X(0) - O2 = qml.X(0) + O2 = qml.X(1) @qml.qnode(dev, interface="jax") def qnode(t1, t2): - return qml.expval(qml.prod(O1, qml.RX(t1, 0), O2, qml.RX(t2, 0))) + qml.RX(t1, 0) + qml.RY(t2, 1) + return qml.expval(qml.prod(O1, O2)) t1, t2 = 0.5, 1.0 assert qml.math.allclose(qnode(t1, t2), jax.jit(qnode)(t1, t2)) diff --git a/tests/workflow/test_construct_batch.py b/tests/workflow/test_construct_batch.py index 6892065bc72..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] From 51b09969bdfd9bd31994f5bd7520bd10062f75d8 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Fri, 21 Jun 2024 10:23:26 -0400 Subject: [PATCH 06/10] update test --- tests/devices/qubit/test_measure.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/devices/qubit/test_measure.py b/tests/devices/qubit/test_measure.py index e7426f7f437..d0c618311cf 100644 --- a/tests/devices/qubit/test_measure.py +++ b/tests/devices/qubit/test_measure.py @@ -179,14 +179,9 @@ def test_op_math_observable_jit_compatible(self): dev = qml.device("default.qubit", wires=4) - O1 = qml.X(0) - O2 = qml.X(1) - @qml.qnode(dev, interface="jax") def qnode(t1, t2): - qml.RX(t1, 0) - qml.RY(t2, 1) - return qml.expval(qml.prod(O1, O2)) + 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)) From 03836b421a5155107adc2c618058f3fb3242f870 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 2 Jul 2024 10:56:08 -0400 Subject: [PATCH 07/10] test coverage --- pennylane/devices/default_qubit.py | 13 +++-- pennylane/devices/preprocess.py | 4 +- .../test_default_qubit_preprocessing.py | 47 +++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 4079722048b..aebbe4bbeeb 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -85,6 +85,9 @@ def observable_accepts_sampling(obs: qml.operation.Operator) -> bool: 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) @@ -100,19 +103,19 @@ def observable_accepts_analytic(obs: qml.operation.Operator, is_expval=False) -> 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 obs.has_diagonalizing_gates: + if is_expval and isinstance(obs, (qml.ops.SparseHamiltonian, qml.ops.Hermitian)): return True - if is_expval: - return isinstance(obs, (qml.ops.SparseHamiltonian, qml.ops.Hermitian)) - - return True + return obs.has_diagonalizing_gates def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: diff --git a/pennylane/devices/preprocess.py b/pennylane/devices/preprocess.py index 324b6c8ddb0..72db6e5f6e7 100644 --- a/pennylane/devices/preprocess.py +++ b/pennylane/devices/preprocess.py @@ -477,7 +477,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..bfcefde2542 100644 --- a/tests/devices/default_qubit/test_default_qubit_preprocessing.py +++ b/tests/devices/default_qubit/test_default_qubit_preprocessing.py @@ -288,6 +288,53 @@ 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), + # 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.""" From b15e9d7dea5103250203f798146895cbd4fb34b7 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 2 Jul 2024 11:49:46 -0400 Subject: [PATCH 08/10] changelog --- doc/releases/changelog-dev.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 22f268d7bcb..47d2e545997 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -3,11 +3,15 @@ # 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)

Improvements 🛠

+* 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 💔

Deprecations 👋

@@ -20,4 +24,5 @@ This release contains contributions from (in alphabetical order): +Astral Cai, Yushao Chen. \ No newline at end of file From c4cebae12d6da6451fdd7d7dcc76414550fc311e Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 11 Jul 2024 14:12:59 -0400 Subject: [PATCH 09/10] add test --- .../test_default_qubit_preprocessing.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/devices/default_qubit/test_default_qubit_preprocessing.py b/tests/devices/default_qubit/test_default_qubit_preprocessing.py index bfcefde2542..4683b50005b 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 + @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) @@ -312,6 +323,11 @@ def has_matrix(self): 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), From 41ae88656a0d1196f0469544d614cff1324fed03 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 11 Jul 2024 14:35:05 -0400 Subject: [PATCH 10/10] make pylint happy --- tests/devices/default_qubit/test_default_qubit_preprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devices/default_qubit/test_default_qubit_preprocessing.py b/tests/devices/default_qubit/test_default_qubit_preprocessing.py index 4683b50005b..cb1e8707008 100644 --- a/tests/devices/default_qubit/test_default_qubit_preprocessing.py +++ b/tests/devices/default_qubit/test_default_qubit_preprocessing.py @@ -50,7 +50,7 @@ def has_matrix(self): class HasDiagonalizingGatesOp(qml.operation.Operator): """Dummy observable that has diagonalizing gates.""" - # pylint: disable=arguments-renamed, invalid-overridden-method + # pylint: disable=arguments-renamed,invalid-overridden-method,no-self-argument @classproperty def has_diagonalizing_gates(cls): return True