diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 71880d7607a..ecda32c06ea 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -209,6 +209,9 @@ [stim](https://github.com/quantumlib/Stim) `v1.13.0`. [(#5409)](https://github.com/PennyLaneAI/pennylane/pull/5409) +* `qml.specs` now returns information regarding algorithmic errors for the qnode as well. + [(#5464)](https://github.com/PennyLaneAI/pennylane/pull/5464) + * `qml.transforms.hamiltonian_expand` can now handle multi-term observables with a constant offset. [(#5414)](https://github.com/PennyLaneAI/pennylane/pull/5414) diff --git a/pennylane/resource/error/__init__.py b/pennylane/resource/error/__init__.py index fd758061035..8abe9f434bc 100644 --- a/pennylane/resource/error/__init__.py +++ b/pennylane/resource/error/__init__.py @@ -17,4 +17,4 @@ """ from .trotter_error import _one_norm_error, _commutator_error -from .error import AlgorithmicError, ErrorOperation, SpectralNormError +from .error import AlgorithmicError, ErrorOperation, SpectralNormError, _compute_algo_error diff --git a/pennylane/resource/error/error.py b/pennylane/resource/error/error.py index acb7a5bf424..53a5ca39c9d 100644 --- a/pennylane/resource/error/error.py +++ b/pennylane/resource/error/error.py @@ -15,6 +15,7 @@ Stores classes and logic to define and track algorithmic error in a quantum workflow. """ from abc import ABC, abstractmethod +from typing import Dict import pennylane as qml from pennylane.operation import Operation, Operator @@ -142,3 +143,26 @@ def get_error(approximate_op: Operator, exact_op: Operator): m1 = qml.matrix(exact_op, wire_order=wire_order) m2 = qml.matrix(approximate_op, wire_order=wire_order) return qml.math.max(qml.math.svd(m1 - m2, compute_uv=False)) + + +def _compute_algo_error(tape) -> Dict[str, AlgorithmicError]: + """Given a quantum circuit (tape), this function computes the algorithmic error + generated by standard PennyLane operations. + + Args: + tape (.QuantumTape): The quantum circuit for which we compute errors + + Returns: + dict[str->.AlgorithmicError]: dict with error name and combined error as key-value pair + """ + + algo_errors = {} + for op in tape.operations: + if isinstance(op, ErrorOperation): + op_error = op.error() + error_name = op_error.__class__.__name__ + algo_error = algo_errors.get(error_name, None) + error_value = op_error if algo_error is None else algo_error.combine(op_error) + algo_errors[error_name] = error_value + + return algo_errors diff --git a/pennylane/resource/specs.py b/pennylane/resource/specs.py index 1112e1b9ac1..d515ef84d6c 100644 --- a/pennylane/resource/specs.py +++ b/pennylane/resource/specs.py @@ -52,19 +52,23 @@ def specs(qnode, max_expansion=None, expansion_strategy=None): .. code-block:: python3 x = np.array([0.1, 0.2]) + hamiltonian = qml.dot([1.0, 0.5], [qml.X(0), qml.Y(0)]) dev = qml.device('default.qubit', wires=2) @qml.qnode(dev, diff_method="parameter-shift", shifts=np.pi / 4) def circuit(x, add_ry=True): qml.RX(x[0], wires=0) qml.CNOT(wires=(0,1)) + qml.TrotterProduct(hamiltonian, time=1.0, n=4, order=2) if add_ry: qml.RY(x[1], wires=1) + qml.TrotterProduct(hamiltonian, time=1.0, n=4, order=4) return qml.probs(wires=(0,1)) >>> qml.specs(circuit)(x, add_ry=False) - {'resources': Resources(num_wires=2, num_gates=2, gate_types=defaultdict(, {'RX': 1, 'CNOT': 1}), - gate_sizes=defaultdict(, {1: 1, 2: 1}), depth=2, shots=Shots(total_shots=None, shot_vector=())), + {'resources': Resources(num_wires=2, num_gates=4, gate_types=defaultdict(, {'RX': 1, 'CNOT': 1, 'TrotterPro + duct': 2}}), gate_sizes=defaultdict(, {1: 3, 2: 1}), depth=4, shots=Shots(total_shots=None, shot_vector=())), + 'errors': {'SpectralNormError': SpectralNormError(0.42998560822421455)}, 'num_observables': 1, 'num_diagonalizing_gates': 0, 'num_trainable_params': 1, @@ -74,7 +78,7 @@ def circuit(x, add_ry=True): 'gradient_options': {'shifts': 0.7853981633974483}, 'interface': 'auto', 'diff_method': 'parameter-shift', - 'gradient_fn': 'pennylane.transforms.core.transform_dispatcher.param_shift', + 'gradient_fn': 'pennylane.gradients.parameter_shift.param_shift', 'num_gradient_executions': 2} """ @@ -87,6 +91,7 @@ def specs_qnode(*args, **kwargs): * ``"num_observables"`` number of observables in the qnode * ``"num_diagonalizing_gates"`` number of diagonalizing gates required for execution of the qnode * ``"resources"``: a :class:`~.resource.Resources` object containing resource quantities used by the qnode + * ``"errors"``: combined algorithmic errors from the quantum operations executed by the qnode * ``"num_used_wires"``: number of wires used by the circuit * ``"num_device_wires"``: number of wires in device * ``"depth"``: longest path in directed acyclic graph representation diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index f450bf0161f..3a681d2f61f 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -979,13 +979,14 @@ def specs(self): gate_sizes: {1: 4, 2: 2} """ + # pylint: disable=protected-access if self._specs is None: - resources = qml.resource.resource._count_resources( - self - ) # pylint: disable=protected-access + resources = qml.resource.resource._count_resources(self) + algo_errors = qml.resource.error._compute_algo_error(self) self._specs = { "resources": resources, + "errors": algo_errors, "num_observables": len(self.observables), "num_diagonalizing_gates": len(self.diagonalizing_gates), "num_trainable_params": self.num_params, diff --git a/tests/resource/test_error/test_error.py b/tests/resource/test_error/test_error.py index 8e07e0c1c03..d1b171bab44 100644 --- a/tests/resource/test_error/test_error.py +++ b/tests/resource/test_error/test_error.py @@ -20,7 +20,12 @@ import numpy as np import pennylane as qml -from pennylane.resource.error import AlgorithmicError, SpectralNormError, ErrorOperation +from pennylane.resource.error import ( + AlgorithmicError, + SpectralNormError, + ErrorOperation, + _compute_algo_error, +) from pennylane.operation import Operation @@ -181,3 +186,99 @@ class NoErrorOp(ErrorOperation): num_wires = 3 _ = NoErrorOp(wires=[1, 2, 3]) + + +class MultiplicativeError(AlgorithmicError): + """Multiplicative error object""" + + def combine(self, other): + return self.__class__(self.error * other.error) + + def __repr__(self): + """Return formal string representation.""" + return f"MultiplicativeError({self.error})" + + +class AdditiveError(AlgorithmicError): + """Additive error object""" + + def combine(self, other): + return self.__class__(self.error + other.error) + + def __repr__(self): + """Return formal string representation.""" + return f"AdditiveError({self.error})" + + +class CustomErrorOp1(ErrorOperation): + """Custome error operation with multiplicative error""" + + def __init__(self, phase, wires): + self.phase = phase + super().__init__(phase, wires=wires) + + def error(self, *args, **kwargs): + return MultiplicativeError(self.phase) + + +class CustomErrorOp2(ErrorOperation): + """Custome error with additive error""" + + def __init__(self, flips, wires): + self.flips = flips + super().__init__(flips, wires=wires) + + def error(self, *args, **kwargs): + return AdditiveError(self.flips) + + +_HAMILTONIAN = qml.dot([1.0, -0.5], [qml.X(0) @ qml.Y(1), qml.Y(0) @ qml.Y(1)]) + + +class TestSpecAndTracker: + """Test capture of ErrorOperation in specs and tracker.""" + + # TODO: remove this when support for below is present + # little hack for stopping device-level decomposition for custom ops + @staticmethod + def preprocess(execution_config=qml.devices.DefaultExecutionConfig): + """A vanilla preprocesser""" + return qml.transforms.core.TransformProgram(), execution_config + + dev = qml.device("null.qubit", wires=2) + dev.preprocess = preprocess.__func__ + + @staticmethod + @qml.qnode(dev) + def circuit(): + """circuit with custom ops""" + qml.TrotterProduct(_HAMILTONIAN, time=1.0, n=4, order=2) + CustomErrorOp1(0.31, [0]) + CustomErrorOp2(0.12, [1]) + qml.TrotterProduct(_HAMILTONIAN, time=1.0, n=4, order=4) + CustomErrorOp1(0.24, [1]) + CustomErrorOp2(0.73, [0]) + return qml.state() + + errors_types = ["MultiplicativeError", "AdditiveError", "SpectralNormError"] + + def test_computation(self): + """Test that _compute_algo_error are adding up errors as expected.""" + + _ = self.circuit() + algo_errors = _compute_algo_error(self.circuit.qtape) + assert len(algo_errors) == 3 + assert all(error in algo_errors for error in self.errors_types) + assert algo_errors["MultiplicativeError"].error == 0.31 * 0.24 + assert algo_errors["AdditiveError"].error == 0.73 + 0.12 + assert algo_errors["SpectralNormError"].error == 0.25 + 0.17998560822421455 + + def test_specs(self): + """Test that specs are tracking errors as expected.""" + + algo_errors = qml.specs(self.circuit)()["errors"] + assert len(algo_errors) == 3 + assert all(error in algo_errors for error in self.errors_types) + assert algo_errors["MultiplicativeError"].error == 0.31 * 0.24 + assert algo_errors["AdditiveError"].error == 0.73 + 0.12 + assert algo_errors["SpectralNormError"].error == 0.25 + 0.17998560822421455 diff --git a/tests/resource/test_specs.py b/tests/resource/test_specs.py index be31aaf1365..788348d2496 100644 --- a/tests/resource/test_specs.py +++ b/tests/resource/test_specs.py @@ -25,7 +25,7 @@ class TestSpecsTransform: """Tests for the transform specs using the QNode""" @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 11), ("parameter-shift", 12), ("adjoint", 11)] + "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)] ) def test_empty(self, diff_method, len_info): dev = qml.device("default.qubit", wires=1) @@ -57,7 +57,7 @@ def circ(): assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift" @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 11), ("parameter-shift", 12), ("adjoint", 11)] + "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)] ) def test_specs(self, diff_method, len_info): """Test the specs transforms works in standard situations""" @@ -102,7 +102,7 @@ def circuit(x, y, add_RY=True): assert info["num_gradient_executions"] == 6 @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 11), ("parameter-shift", 12), ("adjoint", 11)] + "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)] ) def test_specs_state(self, diff_method, len_info): """Test specs works when state returned""" @@ -173,7 +173,7 @@ def test_expansion_strategy(self): info = qml.specs(circuit, expansion_strategy="device")(params) assert circuit.expansion_strategy == "gradient" - assert len(info) == 11 + assert len(info) == 12 def test_gradient_transform(self): """Test that a gradient transform is properly labelled""" diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py index 9c002c4baf4..8493a8a80b3 100644 --- a/tests/tape/test_qscript.py +++ b/tests/tape/test_qscript.py @@ -448,7 +448,7 @@ def test_empty_qs_specs(self): assert qs.specs["num_diagonalizing_gates"] == 0 assert qs.specs["num_trainable_params"] == 0 - assert len(qs.specs) == 4 + assert len(qs.specs) == 5 assert qs._specs is qs.specs @@ -460,7 +460,7 @@ def test_specs_tape(self, make_script): specs = qs.specs assert qs._specs is specs - assert len(specs) == 4 + assert len(specs) == 5 gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1}) gate_sizes = defaultdict(int, {1: 3, 2: 1}) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 5a1e42d72f7..3f8e8138035 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -551,7 +551,7 @@ def test_specs_empty_tape(self, make_empty_tape): assert tape.specs["num_diagonalizing_gates"] == 0 assert tape.specs["num_trainable_params"] == 0 - assert len(tape.specs) == 4 + assert len(tape.specs) == 5 def test_specs_tape(self, make_tape): """Tests that regular tapes return correct specifications""" @@ -559,7 +559,7 @@ def test_specs_tape(self, make_tape): specs = tape.specs - assert len(specs) == 4 + assert len(specs) == 5 gate_sizes = defaultdict(int, {1: 3, 2: 1}) gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1}) @@ -577,7 +577,7 @@ def test_specs_add_to_tape(self, make_extendible_tape): tape = make_extendible_tape specs1 = tape.specs - assert len(specs1) == 4 + assert len(specs1) == 5 gate_sizes = defaultdict(int, {1: 3, 2: 1}) gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1}) @@ -599,7 +599,7 @@ def test_specs_add_to_tape(self, make_extendible_tape): specs2 = tape.specs - assert len(specs2) == 4 + assert len(specs2) == 5 gate_sizes = defaultdict(int, {1: 4, 2: 2}) gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 2, "RZ": 1})