Skip to content

Commit

Permalink
Add algorithmic errors tracking to qml.specs (#5464)
Browse files Browse the repository at this point in the history
**Context:** Update `qml.specs` to track and combine error

**Description of the Change:** Adds a `_compute_algo_error` method that
combines the individual errors of similar types. Further updates `specs`
method of `QuantumScript` to use the prior method for the required
computation in a similar fashion to `resources`.
 
**Benefits:** `specs` will track algorithmic errors. 

**Possible Drawbacks:** N/A

**Related GitHub Issues:** N/A

---------

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>
  • Loading branch information
obliviateandsurrender and Jaybsoni committed Apr 12, 2024
1 parent b2b0c36 commit d5e3b60
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 18 deletions.
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pennylane/resource/error/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions pennylane/resource/error/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
11 changes: 8 additions & 3 deletions pennylane/resource/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(<class 'int'>, {'RX': 1, 'CNOT': 1}),
gate_sizes=defaultdict(<class 'int'>, {1: 1, 2: 1}), depth=2, shots=Shots(total_shots=None, shot_vector=())),
{'resources': Resources(num_wires=2, num_gates=4, gate_types=defaultdict(<class 'int'>, {'RX': 1, 'CNOT': 1, 'TrotterPro
duct': 2}}), gate_sizes=defaultdict(<class 'int'>, {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,
Expand All @@ -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}
"""
Expand All @@ -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
Expand Down
7 changes: 4 additions & 3 deletions pennylane/tape/qscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
103 changes: 102 additions & 1 deletion tests/resource/test_error/test_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions tests/resource/test_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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"""
Expand Down
4 changes: 2 additions & 2 deletions tests/tape/test_qscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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})
Expand Down
8 changes: 4 additions & 4 deletions tests/tape/test_tape.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,15 +551,15 @@ 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"""
tape = 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})
Expand All @@ -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})
Expand All @@ -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})
Expand Down

0 comments on commit d5e3b60

Please sign in to comment.