diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 938b29fc65d..3f970d489d8 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -207,6 +207,10 @@
Breaking changes 💔
+* The output format of `qml.specs` has been restructured into a dataclass to streamline the outputs.
+ Some legacy information has been removed from the new output format.
+ [(#8713)](https://github.com/PennyLaneAI/pennylane/pull/8713)
+
* The unified compiler, implemented in the `qml.compiler.python_compiler` submodule, has been removed from PennyLane.
It has been migrated to Catalyst, available as `catalyst.python_interface`.
[(#8662)](https://github.com/PennyLaneAI/pennylane/pull/8662)
diff --git a/pennylane/devices/_qubit_device.py b/pennylane/devices/_qubit_device.py
index 09f7cc2a864..69b1674626a 100644
--- a/pennylane/devices/_qubit_device.py
+++ b/pennylane/devices/_qubit_device.py
@@ -44,7 +44,6 @@
SampleMeasurement,
SampleMP,
ShadowExpvalMP,
- Shots,
StateMeasurement,
StateMP,
VarianceMP,
@@ -52,7 +51,6 @@
)
from pennylane.operation import Operation, Operator, operation_derivative
from pennylane.ops import MeasurementValue, MidMeasure, Rot, X, Y, Z, adjoint
-from pennylane.resource import Resources
from pennylane.tape import QuantumScript
from pennylane.wires import Wires
@@ -314,19 +312,11 @@ def execute(self, circuit, **kwargs):
self._num_executions += 1
if self.tracker.active:
- shots_from_dev = self._shots if not self.shot_vector else self._raw_shot_sequence
- tape_resources = circuit.specs["resources"]
-
- resources = Resources( # temporary until shots get updated on tape !
- tape_resources.num_wires,
- tape_resources.num_gates,
- tape_resources.gate_types,
- tape_resources.gate_sizes,
- tape_resources.depth,
- Shots(shots_from_dev),
- )
self.tracker.update(
- executions=1, shots=self._shots, results=results, resources=resources
+ executions=1,
+ shots=self._shots,
+ results=results,
+ resources=circuit.specs["resources"],
)
self.tracker.record()
diff --git a/pennylane/resource/__init__.py b/pennylane/resource/__init__.py
index b47d532c9c2..273fa708ea6 100644
--- a/pennylane/resource/__init__.py
+++ b/pennylane/resource/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2022 Xanadu Quantum Technologies Inc.
+# Copyright 2018-2025 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -18,14 +18,8 @@
.. seealso::
The :mod:`~.estimator` module for higher level resource estimation of quantum programs.
-.. warning::
- The functions ``estimate_error``, ``estimate_shots`` and the classes ``DoubleFactorization``,
- ``FirstQuantization`` have been moved to the :mod:`pennylane.estimator` module.
- Accessing them from the :mod:`pennylane.resource` module is deprecated and will be removed
- in v0.45.
-
-Circuit specifications
-----------------------
+Circuit Specifications (specs)
+------------------------------
.. currentmodule:: pennylane
@@ -34,6 +28,18 @@
~specs
+Circuit Specification Classes and Utilities
+-------------------------------------------
+
+.. currentmodule:: pennylane.resource
+
+.. autosummary::
+ :toctree: api
+
+ ~CircuitSpecs
+ ~SpecsResources
+
+ ~resources_from_tape
Error Tracking
--------------
@@ -47,6 +53,12 @@
~SpectralNormError
~ErrorOperation
+.. warning::
+ The functions ``estimate_error``, ``estimate_shots`` and the classes ``DoubleFactorization``,
+ ``FirstQuantization`` have been moved to the :mod:`pennylane.estimator` module.
+ Accessing them from the :mod:`pennylane.resource` module is deprecated and will be removed
+ in v0.45.
+
Resource Classes
----------------
@@ -75,13 +87,14 @@
Tracking Resources for Custom Operations
----------------------------------------
-We can use the :code:`null.qubit` device with the :class:`pennylane.Tracker` to track the resources
-used in a quantum circuit with custom operations without execution.
+We can use the :mod:`null.qubit ` device with :class:`pennylane.Tracker`
+to track the resources used in a quantum circuit with custom operations without execution.
.. code-block:: python
from functools import partial
from pennylane import numpy as pnp
+ from pennylane.resource import Resources, ResourcesOperation
class MyCustomAlgorithm(ResourcesOperation):
num_wires = 2
@@ -112,25 +125,32 @@ def circuit(theta):
We can examine the resources by accessing the :code:`resources` key:
->>> resources_lst = tracker.history['resources']
->>> print(resources_lst[0])
-num_wires: 3
-num_gates: 7
-depth: 5
-shots: Shots(total=100)
-gate_types:
-{'RZ': 1, 'CNOT': 2, 'Hadamard': 2, 'PauliZ': 2}
-gate_sizes:
-{1: 5, 2: 2}
+ >>> resources_lst = tracker.history['resources']
+ >>> print(resources_lst[0])
+ Total qubit allocations: 3
+ Total gates: 7
+ Circuit depth: 5
+
+ Gate types:
+ RZ: 1
+ CNOT: 2
+ Hadamard: 2
+ PauliZ: 2
+
+ Measurements:
+ expval(PauliZ): 1
"""
from .error import AlgorithmicError, ErrorOperation, SpectralNormError
from .resource import (
Resources,
ResourcesOperation,
+ SpecsResources,
+ CircuitSpecs,
add_in_series,
add_in_parallel,
mul_in_series,
mul_in_parallel,
+ resources_from_tape,
substitute,
)
from .specs import specs
diff --git a/pennylane/resource/resource.py b/pennylane/resource/resource.py
index 3a65b2f5473..78b71c366a4 100644
--- a/pennylane/resource/resource.py
+++ b/pennylane/resource/resource.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2023 Xanadu Quantum Technologies Inc.
+# Copyright 2018-2025 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -19,31 +19,17 @@
import copy
from abc import abstractmethod
from collections import defaultdict
-from dataclasses import dataclass, field
+from dataclasses import asdict, dataclass, field, fields
from typing import Any
-from pennylane.measurements import Shots, add_shots
+from pennylane.measurements import MeasurementProcess, Shots, add_shots
from pennylane.operation import Operation
+from pennylane.ops.op_math import Controlled, ControlledOp
from pennylane.tape import QuantumScript
from .error import _compute_algo_error
-class SpecsDict(dict):
- """A special dictionary for storing the specs of a circuit. Used to customize ``KeyError`` messages."""
-
- def __getitem__(self, __k):
- if __k == "num_diagonalizing_gates":
- raise KeyError(
- "num_diagonalizing_gates is no longer in specs due to the ambiguity of the definition "
- "and extreme performance costs."
- )
- try:
- return super().__getitem__(__k)
- except KeyError as e:
- raise KeyError(f"key {__k} not available. Options are {set(self.keys())}") from e
-
-
@dataclass(frozen=True)
class Resources:
r"""Contains attributes which store key resources such as number of gates, number of wires, shots,
@@ -105,9 +91,9 @@ class Resources:
num_wires: int = 0
num_gates: int = 0
- gate_types: dict = field(default_factory=dict)
- gate_sizes: dict = field(default_factory=dict)
- depth: int = 0
+ gate_types: dict[str, int] = field(default_factory=dict)
+ gate_sizes: dict[int, int] = field(default_factory=dict)
+ depth: int | None = 0
shots: Shots = field(default_factory=Shots)
def __add__(self, other: Resources):
@@ -230,6 +216,318 @@ def __str__(self):
def _ipython_display_(self):
"""Displays __str__ in ipython instead of __repr__"""
+ # See https://ipython.readthedocs.io/en/stable/config/integrating.html#custom-methods
+ print(str(self))
+
+
+# TODO: Would be better to have SpecsResources inherit from Resources directly, but there are too
+# many extra fields that are unwanted. Would be worth refactoring in the future.
+@dataclass(frozen=True)
+class SpecsResources:
+ """
+ Class for storing resource information for a quantum circuit. Contains attributes which store
+ key resources such as gate counts, number of qubit allocations, measurements, and circuit depth.
+
+ Args:
+ gate_types (dict[str, int]): A dictionary mapping gate names to their counts.
+ gate_sizes (dict[int, int]): A dictionary mapping gate sizes to their counts.
+ measurements (dict[str, int]): A dictionary mapping measurements to their counts.
+ num_allocs (int): The number of unique qubit allocations. For circuits that do not use
+ dynamic wires, this should be equal to the number of device wires.
+ depth (int | None): The depth of the circuit, or None if not computed.
+
+ Properties:
+ num_gates (int): The total number of gates in the circuit (computed from `gate_types`).
+
+ .. details::
+
+ Methods have been provided to allow pretty-printing, as well as
+ indexing into it as a dictionary. See examples below.
+
+ **Example**
+
+ >>> from pennylane.resource import SpecsResources
+ >>> res = SpecsResources(
+ ... gate_types={'Hadamard': 1, 'CNOT': 1},
+ ... gate_sizes={1: 1, 2: 1},
+ ... measurements={'expval(PauliZ)': 1},
+ ... num_allocs=2,
+ ... depth=2
+ ... )
+
+ >>> print(res.num_gates)
+ 2
+
+ >>> print(res["num_gates"])
+ 2
+
+ >>> print(res)
+ Total qubit allocations: 2
+ Total gates: 2
+ Circuit depth: 2
+
+ Gate types:
+ Hadamard: 1
+ CNOT: 1
+
+ Measurements:
+ expval(PauliZ): 1
+ """
+
+ gate_types: dict[str, int]
+ gate_sizes: dict[int, int]
+ measurements: dict[str, int]
+ num_allocs: int
+ depth: int | None = None
+
+ def __post_init__(self):
+ if sum(self.gate_types.values()) != sum(self.gate_sizes.values()):
+ raise ValueError(
+ "Inconsistent gate counts: `gate_types` and `gate_sizes` describe different amounts of gates."
+ )
+
+ def to_dict(self) -> dict[str, Any]:
+ """Convert the SpecsResources to a dictionary."""
+
+ # Need to explicitly include properties
+ d = asdict(self)
+ d["num_gates"] = self.num_gates
+
+ return d
+
+ def __getitem__(self, key):
+ if key in (field.name for field in fields(self)):
+ return getattr(self, key)
+
+ match key:
+ # Fields that used to be included in specs output prior to PL version 0.44
+ case "shots":
+ raise KeyError(
+ "shots is no longer included within specs's resources, check the top-level object instead."
+ )
+ case "num_wires":
+ raise KeyError(
+ "num_wires has been renamed to num_allocs to more accurate describe what it measures."
+ )
+ case "num_gates":
+ # As a property, this needs to be handled differently to the true fields
+ return self.num_gates
+
+ raise KeyError(
+ f"key '{key}' not available. Options are {[field.name for field in fields(self)]}"
+ )
+
+ @property
+ def num_gates(self) -> int:
+ """Total number of gates in the circuit."""
+ return sum(self.gate_types.values())
+
+ def to_pretty_str(self, preindent: int = 0) -> str:
+ """
+ Pretty string representation of the SpecsResources object.
+
+ Args:
+ preindent (int): Number of spaces to prepend to each line.
+
+ Returns:
+ str: A pretty representation of this object.
+ """
+ prefix = " " * preindent
+ lines = []
+
+ lines.append(f"{prefix}Total qubit allocations: {self.num_allocs}")
+ lines.append(f"{prefix}Total gates: {self.num_gates}")
+ lines.append(
+ f"{prefix}Circuit depth: {self.depth if self.depth is not None else 'Not computed'}"
+ )
+
+ lines.append("") # Blank line
+
+ lines.append(f"{prefix}Gate types:")
+ if not self.gate_types:
+ lines.append(prefix + " No gates.")
+ else:
+ for gate, count in self.gate_types.items():
+ lines.append(f"{prefix} {gate}: {count}")
+
+ lines.append("") # Blank line
+
+ lines.append(f"{prefix}Measurements:")
+ if not self.measurements:
+ lines.append(prefix + " No measurements.")
+ else:
+ for meas, count in self.measurements.items():
+ lines.append(f"{prefix} {meas}: {count}")
+
+ return "\n".join(lines)
+
+ # Leave repr and str methods separate for simple and pretty printing
+ def __str__(self) -> str:
+ return self.to_pretty_str()
+
+ def _ipython_display_(self): # pragma: no cover
+ """Displays __str__ in ipython instead of __repr__"""
+ # See https://ipython.readthedocs.io/en/stable/config/integrating.html#custom-methods
+ print(str(self))
+
+
+@dataclass(frozen=True)
+class CircuitSpecs:
+ """
+ Class for storing specifications of a qnode. Contains resource information as well as additional
+ data such as the device, number of shots, and level of the requested specs.
+
+ Args:
+ device_name (str): The name of the device used.
+ num_device_wires (int): The number of wires on the device.
+ shots (Shots): The shots configuration used.
+ level (Any): The level of the specs (see :func:`~pennylane.specs` for more details).
+ resources (SpecsResources | dict[int | str, SpecsResources]): The resource specifications.
+ Depending on the ``level`` chosen, this may be a single :class:`.SpecsResources` object
+ or a dictionary of multiple :class:`.SpecsResources` objects.
+
+ .. details::
+
+ Some helpful methods have been added to this data class to allow pretty-printing, as well as
+ indexing into it as a dictionary. See examples below.
+
+ **Example**
+
+ >>> from pennylane.resource import SpecsResources, CircuitSpecs
+ >>> specs = CircuitSpecs(
+ ... device_name="default.qubit",
+ ... num_device_wires=2,
+ ... shots=Shots(1000),
+ ... level="device",
+ ... resources=SpecsResources(
+ ... gate_types={"RX": 2, "CNOT": 1},
+ ... gate_sizes={1: 2, 2: 1},
+ ... measurements={"expval(PauliZ)": 1},
+ ... num_allocs=2,
+ ... depth=3,
+ ... ),
+ ... )
+
+ >>> print(specs.num_device_wires)
+ 2
+
+ >>> print(specs["num_device_wires"])
+ 2
+
+ >>> print(specs)
+ Device: default.qubit
+ Device wires: 2
+ Shots: Shots(total=1000)
+ Level: device
+
+ Resource specifications:
+ Total qubit allocations: 2
+ Total gates: 3
+ Circuit depth: 3
+
+ Gate types:
+ RX: 2
+ CNOT: 1
+
+ Measurements:
+ expval(PauliZ): 1
+ """
+
+ device_name: str | None = None
+ num_device_wires: int | None = None
+ shots: Shots | None = None
+ level: Any = None
+ resources: (
+ SpecsResources
+ | list[SpecsResources]
+ | dict[int | str, SpecsResources | list[SpecsResources]]
+ | None
+ ) = None
+
+ def to_dict(self) -> dict[str, Any]:
+ """Convert the CircuitSpecs to a dictionary."""
+ d = asdict(self)
+
+ # Replace Resources objects with their dict representations
+ if isinstance(self.resources, SpecsResources):
+ d["resources"] = self.resources.to_dict()
+ elif isinstance(self.resources, list):
+ d["resources"] = [r.to_dict() for r in self.resources]
+ elif isinstance(self.resources, dict):
+ d["resources"] = {
+ k: (v.to_dict() if isinstance(v, SpecsResources) else [r.to_dict() for r in v])
+ for k, v in self.resources.items()
+ }
+
+ return d
+
+ def __getitem__(self, key):
+ if key in (field.name for field in fields(self)):
+ return getattr(self, key)
+
+ match key:
+ # Fields that used to be included in specs output prior to PL version 0.44
+ case "num_observables":
+ raise KeyError(
+ "num_observables is no longer in top-level specs and has instead been absorbed into the 'measurements' attribute of the specs's resources."
+ )
+ case "interface" | "diff_method" | "errors" | "num_tape_wires":
+ raise KeyError(f"key '{key}' is no longer included in specs.")
+ case (
+ "gradient_fn"
+ | "gradient_options"
+ | "num_gradient_executions"
+ | "num_trainable_params"
+ ):
+ raise KeyError(
+ f"key '{key}' is no longer included in specs, as specs no longer gathers gradient information."
+ )
+ raise KeyError(
+ f"key '{key}' not available. Options are {[field.name for field in fields(self)]}"
+ )
+
+ def _resources_to_str(self, res) -> str:
+ """Helper for printing resources, prints list or single SpecsResources."""
+ lines = []
+ if isinstance(res, SpecsResources):
+ lines.append(res.to_pretty_str(preindent=2))
+ elif isinstance(res, list):
+ for i, r in enumerate(res):
+ lines.append(f" Batched tape {i}:")
+ lines.append(r.to_pretty_str(preindent=4))
+ lines.append("") # Blank line
+ else:
+ raise ValueError(
+ "Resources must be either a SpecsResources object or a list of SpecsResources objects."
+ ) # pragma: no cover
+
+ return "\n".join(lines)
+
+ # Separate str and repr methods for simple and pretty printing
+ def __str__(self):
+ lines = []
+
+ lines.append(f"Device: {self.device_name}")
+ lines.append(f"Device wires: {self.num_device_wires}")
+ lines.append(f"Shots: {self.shots}")
+ lines.append(f"Level: {self.level}")
+
+ lines.append("") # Blank line
+
+ lines.append("Resource specifications:")
+ if isinstance(self.resources, dict):
+ for level, res in self.resources.items():
+ lines.append(f"Level = {level}:")
+ lines.append(self._resources_to_str(res))
+ lines.append("\n" + "-" * 60 + "\n") # Separator between levels
+ else:
+ lines.append(self._resources_to_str(self.resources))
+
+ return "\n".join(lines).rstrip("\n-")
+
+ def _ipython_display_(self): # pragma: no cover
+ """Displays __str__ in ipython instead of __repr__"""
+ # See https://ipython.readthedocs.io/en/stable/config/integrating.html#custom-methods
print(str(self))
@@ -621,7 +919,9 @@ def substitute(initial_resources: Resources, gate_info: tuple[str, int], replace
# The reason why this function is not a method of the QuantumScript class is
# because we don't want a core module (QuantumScript) to depend on an auxiliary module (Resource).
# The `QuantumScript.specs` property will eventually be deprecated in favor of this function.
-def specs_from_tape(tape: QuantumScript, compute_depth: bool = True) -> SpecsDict[str, Any]:
+def resources_from_tape(
+ tape: QuantumScript, compute_depth: bool = True, compute_errors: bool = False
+) -> SpecsResources | tuple[SpecsResources, dict[str, Any]]:
"""
Extracts the resource information from a quantum circuit (tape).
@@ -634,21 +934,19 @@ def specs_from_tape(tape: QuantumScript, compute_depth: bool = True) -> SpecsDic
tape (.QuantumScript): The quantum circuit for which we extract resources
compute_depth (bool): If True, the depth of the circuit is computed and included in the resources.
If False, the depth is set to None.
-
+ compute_errors (bool): If True, algorithmic errors are computed and returned alongside the resources.
+ Defaults to False.
Returns:
- (.SpecsDict): The specifications extracted from the workflow
+ (SpecsResources | tuple[SpecsResources, dict[str, Any]]): The resources associated with this tape, optionally
+ with algorithmic errors if `compute_errors` is set to True.
"""
resources = _count_resources(tape, compute_depth=compute_depth)
- algo_errors = _compute_algo_error(tape)
-
- return SpecsDict(
- {
- "resources": resources,
- "errors": algo_errors,
- "num_observables": len(tape.observables),
- "num_trainable_params": tape.num_params,
- }
- )
+
+ if compute_errors:
+ algo_errors = _compute_algo_error(tape)
+ return resources, algo_errors
+
+ return resources
def _combine_dict(dict1: dict, dict2: dict):
@@ -675,9 +973,40 @@ def _scale_dict(dict1: dict, scalar: int):
return combined_dict
-def _count_resources(tape: QuantumScript, compute_depth: bool = True) -> Resources:
- """Given a quantum circuit (tape), this function
- counts the resources used by standard PennyLane operations.
+def _obs_to_str(obs) -> str:
+ """Convert an Observable to a string representation for resource counting."""
+ name = obs.name
+ match name:
+ case "Hamiltonian" | "LinearCombination" | "Sum" | "Prod":
+ if name == "LinearCombination":
+ name = "Hamiltonian"
+ return f"{name}(num_wires={obs.num_wires}, num_terms={len(obs.operands)})"
+ case "SProd":
+ return _obs_to_str(obs.base)
+ case "Exp":
+ return f"Exp({_obs_to_str(obs.base)})"
+ case _:
+ return name
+
+
+def _mp_to_str(mp: MeasurementProcess, num_wires: int) -> str:
+ """Convert a MeasurementProcess to a string representation for resource counting."""
+ meas_name = mp._shortname # pylint: disable=protected-access
+ if mp.mv is not None:
+ meas_name += "(mcm)"
+ elif mp.obs is None:
+ meas_wires = len(mp.wires)
+ if meas_wires in (None, 0, num_wires):
+ meas_name += "(all wires)"
+ else:
+ meas_name += f"({meas_wires} wires)"
+ else:
+ meas_name += f"({_obs_to_str(mp.obs)})"
+ return meas_name
+
+
+def _count_resources(tape: QuantumScript, compute_depth: bool = True) -> SpecsResources:
+ """Given a quantum tape, this function counts the resources used by standard PennyLane operations.
Args:
tape (.QuantumScript): The quantum circuit for which we count resources
@@ -685,14 +1014,14 @@ def _count_resources(tape: QuantumScript, compute_depth: bool = True) -> Resourc
If False, the depth is set to None.
Returns:
- (.Resources): The total resources used in the workflow
+ (.SpecsResources): The total resources used in the workflow
"""
+
num_wires = len(tape.wires)
- shots = tape.shots
depth = tape.graph.get_depth() if compute_depth else None
- num_gates = 0
gate_types = defaultdict(int)
+ measurements = defaultdict(int)
gate_sizes = defaultdict(int)
for op in tape.operations:
if isinstance(op, ResourcesOperation):
@@ -703,11 +1032,24 @@ def _count_resources(tape: QuantumScript, compute_depth: bool = True) -> Resourc
for n in op_resource.gate_sizes:
gate_sizes[n] += op_resource.gate_sizes[n]
- num_gates += sum(op_resource.gate_types.values())
-
else:
- gate_types[op.name] += 1
+ gate_name = op.name
+ # pylint: disable=unidiomatic-typecheck
+ if type(op) in (Controlled, ControlledOp):
+ n_ctrls = len(op.control_wires)
+ if n_ctrls > 1:
+ gate_name = f"{n_ctrls}{gate_name}"
+
+ gate_types[gate_name] += 1
gate_sizes[len(op.wires)] += 1
- num_gates += 1
- return Resources(num_wires, num_gates, gate_types, gate_sizes, depth, shots)
+ for meas in tape.measurements:
+ measurements[_mp_to_str(meas, num_wires)] += 1
+
+ return SpecsResources(
+ gate_types=dict(gate_types),
+ gate_sizes=dict(gate_sizes),
+ measurements=dict(measurements),
+ num_allocs=num_wires,
+ depth=depth,
+ )
diff --git a/pennylane/resource/specs.py b/pennylane/resource/specs.py
index 48b86123b01..0ec28e25905 100644
--- a/pennylane/resource/specs.py
+++ b/pennylane/resource/specs.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+# Copyright 2018-2025 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,93 +14,45 @@
"""Code for resource estimation"""
import copy
-import inspect
import json
import os
import warnings
-from collections import defaultdict
from collections.abc import Callable
from functools import partial
-from typing import Any, Literal
import pennylane as qml
-from .resource import Resources, SpecsDict, specs_from_tape
+from .resource import CircuitSpecs, SpecsResources, resources_from_tape
+# Used for device-level qjit resource tracking
_RESOURCE_TRACKING_FILEPATH = "__qml_specs_qjit_resources.json"
-def _get_absolute_import_path(fn):
- return f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
-
-
-def _specs_qnode(qnode, level, compute_depth, *args, **kwargs) -> list[SpecsDict] | SpecsDict:
+def _specs_qnode(qnode, level, compute_depth, *args, **kwargs) -> CircuitSpecs:
"""Returns information on the structure and makeup of provided QNode.
- Dictionary keys:
- * ``"num_operations"`` number of operations in the qnode
- * ``"num_observables"`` number of observables in 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
- * ``"device_name"``: name of QNode device
- * ``"gradient_options"``: additional configurations for gradient computations
- * ``"interface"``: autodiff framework to dispatch to for the qnode execution
- * ``"diff_method"``: a string specifying the differntiation method
- * ``"gradient_fn"``: executable to compute the gradient of the qnode
-
- Potential Additional Information:
- * ``"num_trainable_params"``: number of individual scalars that are trainable
- * ``"num_gradient_executions"``: number of times circuit will execute when
- calculating the derivative
-
Returns:
- dict[str, Union[defaultdict,int]]: dictionaries that contain QNode specifications
+ CircuitSpecs: result object that contains QNode specifications
"""
- infos = []
batch, _ = qml.workflow.construct_batch(qnode, level=level)(*args, **kwargs)
- # These values are the same for the whole batch, so we can just get them once
- config = qml.workflow.construct_execution_config(qnode)(*args, **kwargs)
- gradient_fn = config.gradient_method
-
- for tape in batch:
- info = specs_from_tape(tape, compute_depth)
- info["num_device_wires"] = len(qnode.device.wires or tape.wires)
- info["num_tape_wires"] = tape.num_wires
- info["device_name"] = qnode.device.name
- info["level"] = level
- info["gradient_options"] = qnode.gradient_kwargs
- info["interface"] = qnode.interface
- info["diff_method"] = (
- _get_absolute_import_path(qnode.diff_method)
- if callable(qnode.diff_method)
- else qnode.diff_method
- )
-
- if isinstance(gradient_fn, qml.transforms.core.TransformDispatcher):
- info["gradient_fn"] = _get_absolute_import_path(gradient_fn)
-
- try:
- info["num_gradient_executions"] = len(gradient_fn(tape)[0])
- except Exception as e: # pylint: disable=broad-except
- # In the case of a broad exception, we don't want the `qml.specs` transform
- # to fail. Instead, we simply indicate that the number of gradient executions
- # is not supported for the reason specified.
- info["num_gradient_executions"] = f"NotSupported: {str(e)}"
- else:
- info["gradient_fn"] = gradient_fn
+ resources = [resources_from_tape(tape, compute_depth) for tape in batch]
- infos.append(info)
+ if len(resources) == 1:
+ resources = resources[0]
- return infos[0] if len(infos) == 1 else infos
+ return CircuitSpecs(
+ resources=resources,
+ num_device_wires=len(qnode.device.wires) if qnode.device.wires is not None else None,
+ device_name=qnode.device.name,
+ level=level,
+ shots=qnode.shots,
+ )
# NOTE: Some information is missing from specs_qjit compared to specs_qnode
-def _specs_qjit(qjit, level, compute_depth, *args, **kwargs) -> SpecsDict: # pragma: no cover
+def _specs_qjit(qjit, level, compute_depth, *args, **kwargs) -> CircuitSpecs: # pragma: no cover
# pylint: disable=import-outside-toplevel
# Have to import locally to prevent circular imports as well as accounting for Catalyst not being installed
# Integration tests for this function are within the Catalyst frontend tests, it is not covered by unit tests
@@ -109,13 +61,10 @@ def _specs_qjit(qjit, level, compute_depth, *args, **kwargs) -> SpecsDict: # pr
from ..devices import NullQubit
- # TODO: Determine if its possible to have batched QJIT code / how to handle it
-
if not isinstance(qjit.original_function, qml.QNode):
raise ValueError("qml.specs can only be applied to a QNode or qjit'd QNode")
original_device = qjit.device
- info = SpecsDict()
if level != "device":
raise NotImplementedError(f"Unsupported level argument '{level}' for QJIT'd code.")
@@ -150,41 +99,39 @@ def _specs_qjit(qjit, level, compute_depth, *args, **kwargs) -> SpecsDict: # pr
with open(_RESOURCE_TRACKING_FILEPATH, encoding="utf-8") as f:
resource_data = json.load(f)
- info["resources"] = Resources(
- num_wires=resource_data["num_wires"],
- num_gates=resource_data["num_gates"],
- gate_types=defaultdict(int, resource_data["gate_types"]),
- gate_sizes=defaultdict(
- int, {int(k): v for (k, v) in resource_data["gate_sizes"].items()}
- ),
+ # TODO: Once measurements are tracked for runtime specs, include that data here
+ warnings.warn(
+ "Measurement resource tracking is not yet supported for qjit'd QNodes. "
+ "The returned SpecsResources will have an empty measurements field.",
+ UserWarning,
+ )
+ resources = SpecsResources(
+ gate_types=resource_data["gate_types"],
+ gate_sizes={int(k): v for (k, v) in resource_data["gate_sizes"].items()},
+ measurements={},
+ num_allocs=resource_data["num_wires"],
depth=resource_data["depth"],
- shots=qjit.original_function.shots, # TODO: Can this ever be overriden during compilation?
)
finally:
# Ensure we clean up the resource tracking file
if os.path.exists(_RESOURCE_TRACKING_FILEPATH):
os.remove(_RESOURCE_TRACKING_FILEPATH)
- info["num_device_wires"] = len(original_device.wires)
- info["device_name"] = original_device.name
- info["level"] = level
- info["gradient_options"] = qjit.gradient_kwargs
- info["interface"] = qjit.original_function.interface
- info["diff_method"] = (
- _get_absolute_import_path(qjit.diff_method)
- if callable(qjit.diff_method)
- else qjit.diff_method
+ return CircuitSpecs(
+ resources=resources,
+ num_device_wires=len(qjit.original_function.device.wires),
+ device_name=qjit.original_function.device.name,
+ level=level,
+ shots=qjit.original_function.shots,
)
- return info
-
def specs(
qnode,
- level: Literal["top", "user", "device", "gradient"] | int | slice = "gradient",
+ level: str | int | slice = "gradient",
compute_depth: bool = True,
-) -> Callable[..., list[dict[str, Any]] | dict[str, Any]]:
- r"""Resource information about a quantum circuit.
+) -> Callable[..., CircuitSpecs]:
+ r"""Provides the specifications of a quantum circuit.
This transform converts a QNode into a callable that provides resource information
about the circuit after applying the specified amount of transforms/expansions first.
@@ -193,14 +140,12 @@ def specs(
qnode (.QNode | .QJIT): the QNode to calculate the specifications for.
Keyword Args:
- level (str, int, slice): An indication of what transforms to apply before computing the resource information.
- Check :func:`~.workflow.get_transform_program` for more information on the allowed values and usage details of
- this argument.
- compute_depth (bool): Whether to compute the depth of the circuit. If ``False``, the depth will not be included in the returned information.
+ level (str | int | slice | iter[int]): An indication of which transforms to apply before computing the resource information.
+ compute_depth (bool): Whether to compute the depth of the circuit. If ``False``, the depth will not be included in the returned information. Default: True
Returns:
A function that has the same argument signature as ``qnode``. This function
- returns a dictionary (or a list of dictionaries) of information about qnode structure.
+ returns a :class:`~.resource.CircuitSpecs` object containing the ``qnode`` specifications.
**Example**
@@ -223,29 +168,24 @@ def circuit(x, add_ry=True):
qml.TrotterProduct(Hamiltonian, time=1.0, n=4, order=4)
return qml.probs(wires=(0,1))
- >>> from pprint import pprint
- >>> pprint(qml.specs(circuit)(x, add_ry=False))
- {'device_name': 'default.qubit',
- 'diff_method': 'parameter-shift',
- 'errors': {'SpectralNormError': SpectralNormError(0.42998560822421455)},
- 'gradient_fn': 'pennylane.gradients.parameter_shift.param_shift',
- 'gradient_options': {'shifts': 0.7853981633974483},
- 'interface': 'auto',
- 'level': 'gradient',
- 'num_device_wires': 2,
- 'num_gradient_executions': 2,
- 'num_observables': 1,
- 'num_tape_wires': 2,
- 'num_trainable_params': 1,
- 'resources': Resources(num_wires=2,
- num_gates=98,
- gate_types=defaultdict(,
- {'CNOT': 1,
- 'Evolution': 96,
- 'RX': 1}),
- gate_sizes=defaultdict(, {1: 97, 2: 1}),
- depth=98,
- shots=Shots(total_shots=None, shot_vector=()))}
+ >>> print(qml.specs(circuit)(x, add_ry=False))
+ Device: default.qubit
+ Device wires: 2
+ Shots: Shots(total=None)
+ Level: gradient
+
+ Resource specifications:
+ Total qubit allocations: 2
+ Total gates: 98
+ Circuit depth: 98
+
+ Gate types:
+ RX: 1
+ CNOT: 1
+ Evolution: 96
+
+ Measurements:
+ probs(all wires): 1
.. details::
:title: Usage Details
@@ -274,52 +214,51 @@ def circuit(x):
First, we can check the resource information of the QNode without any modifications. Note that ``level=top`` would
return the same results:
- >>> print(qml.specs(circuit, level=0)(0.1)["resources"])
- num_wires: 2
- num_gates: 6
- depth: 6
- shots: Shots(total=None)
- gate_types:
- {'RandomLayers': 1, 'RX': 2, 'SWAP': 1, 'PauliX': 2}
- gate_sizes:
- {2: 2, 1: 4}
+ >>> print(qml.specs(circuit, level=0)(0.1).resources)
+ Total qubit allocations: 2
+ Total gates: 6
+ Circuit depth: 6
+
+ Gate types:
+ RandomLayers: 1
+ RX: 2
+ SWAP: 1
+ PauliX: 2
+
+ Measurements:
+ expval(Sum(num_wires=2, num_terms=2)): 1
We then check the resources after applying all transforms:
- >>> print(qml.specs(circuit, level="device")(0.1)["resources"])
- num_wires: 2
- num_gates: 2
- depth: 1
- shots: Shots(total=None)
- gate_types:
- {'RY': 1, 'RX': 1}
- gate_sizes:
- {1: 2}
+ >>> print(qml.specs(circuit, level="device")(0.1).resources)
+ Total qubit allocations: 2
+ Total gates: 2
+ Circuit depth: 1
+
+ Gate types:
+ RY: 1
+ RX: 1
+
+ Measurements:
+ expval(Sum(num_wires=2, num_terms=2)): 1
We can also notice that ``SWAP`` and ``PauliX`` are not present in the circuit if we set ``level=2``:
- >>> print(qml.specs(circuit, level=2)(0.1)["resources"])
- num_wires: 2
- num_gates: 3
- depth: 3
- shots: Shots(total=None)
- gate_types:
- {'RandomLayers': 1, 'RX': 2}
- gate_sizes:
- {2: 1, 1: 2}
-
- If we attempt to apply only the ``merge_rotations`` transform, we end up with only one trainable object, which is in ``RandomLayers``:
-
- >>> qml.specs(circuit, level=slice(2, 3))(0.1)["num_trainable_params"]
- 1
-
- However, if we apply all transforms, ``RandomLayers`` is decomposed into an ``RY`` and an ``RX``, giving us two trainable objects:
-
- >>> qml.specs(circuit, level="device")(0.1)["num_trainable_params"]
- 2
-
- If a QNode with a tape-splitting transform is supplied to the function, with the transform included in the desired transforms, a dictionary
- is returned for each resulting tape:
+ >>> print(qml.specs(circuit, level=2)(0.1).resources)
+ Total qubit allocations: 2
+ Total gates: 3
+ Circuit depth: 3
+
+ Gate types:
+ RandomLayers: 1
+ RX: 2
+
+ Measurements:
+ expval(Sum(num_wires=2, num_terms=2)): 1
+
+ If a QNode with a tape-splitting transform is supplied to the function, with the transform included in the
+ desired transforms, the specs output's resources field is instead returned as a list with a
+ :class:`~.resource.CircuitSpecs` for each resulting tape:
.. code-block:: python
@@ -333,8 +272,22 @@ def circuit():
qml.RandomLayers(qml.numpy.array([[1.0, 2.0]]), wires=(0, 1))
return qml.expval(H)
- >>> len(qml.specs(circuit, level="user")())
- 2
+ >>> from pprint import pprint
+ >>> pprint(qml.specs(circuit, level="user")())
+ CircuitSpecs(device_name='default.qubit',
+ num_device_wires=None,
+ shots=Shots(total_shots=None, shot_vector=()),
+ level='user',
+ resources=[SpecsResources(gate_types={'RandomLayers': 1},
+ gate_sizes={2: 1},
+ measurements={'expval(Prod(num_wires=2, num_terms=2))': 1},
+ num_allocs=2,
+ depth=1),
+ SpecsResources(gate_types={'RandomLayers': 1},
+ gate_sizes={2: 1},
+ measurements={'expval(Prod(num_wires=2, num_terms=2))': 1},
+ num_allocs=3,
+ depth=1)])
"""
# pylint: disable=import-outside-toplevel
# Have to import locally to prevent circular imports as well as accounting for Catalyst not being installed
diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py
index 6f6451af7ab..afc251b6a8f 100644
--- a/pennylane/tape/qscript.py
+++ b/pennylane/tape/qscript.py
@@ -983,33 +983,32 @@ def graph(self) -> "qml.CircuitGraph":
return self._graph
@property
- def specs(self) -> "qml.resource.resource.SpecsDict[str, Any]":
+ def specs(self) -> dict[str, Any]:
"""Resource information about a quantum circuit.
Returns:
- SpecsDict[str, Any]: A dictionary containing the specifications of the quantum script.
+ dict[str, Any]: A dictionary containing the specifications of the quantum script.
**Example**
>>> ops = [qml.Hadamard(0), qml.RX(0.26, 1), qml.CNOT((1,0)),
... qml.Rot(1.8, -2.7, 0.2, 0), qml.Hadamard(1), qml.CNOT((0, 1))]
>>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0) @ qml.Z(1))])
- Asking for the specs produces a dictionary of useful information about the circuit:
-
- >>> qscript.specs['num_observables']
- 1
- >>> print(qscript.specs['resources'])
- num_wires: 2
- num_gates: 6
- depth: 4
- shots: Shots(total=None)
- gate_types:
- {'Hadamard': 2, 'RX': 1, 'CNOT': 2, 'Rot': 1}
- gate_sizes:
- {1: 4, 2: 2}
+ Asking for the specs produces a dictionary of useful information about the circuit.
+ Note that this may return slightly different information than running :func:`~.pennylane.specs` on
+ a qnode directly.
+
+ >>> from pprint import pprint
+ >>> pprint(qscript.specs['resources'])
+ SpecsResources(gate_types={'CNOT': 2, 'Hadamard': 2, 'RX': 1, 'Rot': 1},
+ gate_sizes={1: 4, 2: 2},
+ measurements={'expval(Prod(num_wires=2, num_terms=2))': 1},
+ num_allocs=2,
+ depth=4)
"""
if self._specs is None:
- self._specs = qml.resource.resource.specs_from_tape(self)
+ resources, errors = qml.resource.resource.resources_from_tape(self, compute_errors=True)
+ self._specs = {"resources": resources, "shots": self.shots, "errors": errors}
return self._specs
# pylint: disable=too-many-arguments, too-many-positional-arguments
diff --git a/pennylane/transforms/decompose.py b/pennylane/transforms/decompose.py
index 1aa598dc0bf..7cd91596fb0 100644
--- a/pennylane/transforms/decompose.py
+++ b/pennylane/transforms/decompose.py
@@ -729,7 +729,7 @@ def circuit():
return qml.state()
>>> qml.specs(circuit)()["resources"].gate_types
- defaultdict(, {'RZ': 12, 'RX': 7, 'GlobalPhase': 6, 'CZ': 3})
+ {'RZ': 12, 'RX': 7, 'GlobalPhase': 6, 'CZ': 3}
>>> qml.decomposition.disable_graph()
"""
diff --git a/pennylane/transforms/qmc.py b/pennylane/transforms/qmc.py
index c5bb9096771..b36022f07f8 100644
--- a/pennylane/transforms/qmc.py
+++ b/pennylane/transforms/qmc.py
@@ -340,36 +340,26 @@ def qmc():
>>> specs = qml.specs(qmc, level="device")()
>>> from pprint import pprint
>>> pprint(specs)
- {'device_name': 'default.qubit',
- 'diff_method': 'best',
- 'errors': {},
- 'gradient_fn': 'backprop',
- 'gradient_options': {},
- 'interface': 'auto',
- 'level': 'device',
- 'num_device_wires': 12,
- 'num_observables': 1,
- 'num_tape_wires': 12,
- 'num_trainable_params': 15180,
- 'resources': Resources(num_wires=12,
- num_gates=31629,
- gate_types=defaultdict(,
- {'Adjoint(CNOT)': 7812,
- 'Adjoint(QFT)': 1,
- 'Adjoint(RY)': 7560,
- 'CNOT': 7874,
- 'CZ': 126,
- 'Hadamard': 258,
- 'MultiControlledX': 126,
- 'PauliX': 252,
- 'RY': 7620}),
- gate_sizes=defaultdict(,
- {1: 15690,
- 2: 15812,
- 6: 1,
- 7: 126}),
- depth=30357,
- shots=Shots(total_shots=None, shot_vector=()))}
+ CircuitSpecs(device_name='default.qubit',
+ num_device_wires=12,
+ shots=Shots(total_shots=None, shot_vector=()),
+ level='device',
+ resources=SpecsResources(gate_types={'Adjoint(CNOT)': 7812,
+ 'Adjoint(QFT)': 1,
+ 'Adjoint(RY)': 7560,
+ 'CNOT': 7874,
+ 'CZ': 126,
+ 'Hadamard': 258,
+ 'MultiControlledX': 126,
+ 'PauliX': 252,
+ 'RY': 7620},
+ gate_sizes={1: 15690,
+ 2: 15812,
+ 6: 1,
+ 7: 126},
+ measurements={'probs(6 wires)': 1},
+ num_allocs=12,
+ depth=30357))
"""
operations = tape.operations.copy()
wires = Wires(wires)
diff --git a/pennylane/workflow/construct_batch.py b/pennylane/workflow/construct_batch.py
index e67636709fc..90717f2b79c 100644
--- a/pennylane/workflow/construct_batch.py
+++ b/pennylane/workflow/construct_batch.py
@@ -71,7 +71,7 @@ def c():
>>> print(qml.draw(c, level="my_level")())
0: ──RX(0.20)──RX(0.20)─┤ State
>>> qml.specs(c, level="my_level")()['resources'].gate_types
- defaultdict(, {'RX': 2})
+ {'RX': 2}
>>> print(qml.draw(c, level="rotations-merged")())
0: ──RX(0.40)─┤ State
diff --git a/tests/devices/default_qubit/test_default_qubit_tracking.py b/tests/devices/default_qubit/test_default_qubit_tracking.py
index 116a2fe3663..f0032ebb822 100644
--- a/tests/devices/default_qubit/test_default_qubit_tracking.py
+++ b/tests/devices/default_qubit/test_default_qubit_tracking.py
@@ -19,7 +19,7 @@
import pennylane as qml
from pennylane.devices import ExecutionConfig
-from pennylane.resource import Resources
+from pennylane.resource import SpecsResources
class TestTracking:
@@ -55,7 +55,29 @@ def test_tracking_batch(self):
"executions": [1, 1, 1],
"simulations": [1, 1, 1],
"results": [1.0, 1.0, 1.0],
- "resources": [Resources(num_wires=1), Resources(num_wires=1), Resources(num_wires=1)],
+ "resources": [
+ SpecsResources(
+ num_allocs=1,
+ gate_types={},
+ gate_sizes={},
+ measurements={"expval(PauliZ)": 1},
+ depth=0,
+ ),
+ SpecsResources(
+ num_allocs=1,
+ gate_types={},
+ gate_sizes={},
+ measurements={"expval(PauliZ)": 1},
+ depth=0,
+ ),
+ SpecsResources(
+ num_allocs=1,
+ gate_types={},
+ gate_sizes={},
+ measurements={"expval(PauliZ)": 1},
+ depth=0,
+ ),
+ ],
"derivative_batches": [1],
"derivatives": [1],
"errors": [{}, {}, {}],
@@ -72,7 +94,13 @@ def test_tracking_batch(self):
"executions": 1,
"simulations": 1,
"results": 1,
- "resources": Resources(num_wires=1),
+ "resources": SpecsResources(
+ num_allocs=1,
+ gate_types={},
+ gate_sizes={},
+ measurements={"expval(PauliZ)": 1},
+ depth=0,
+ ),
"errors": {},
}
@@ -103,7 +131,16 @@ def test_tracking_execute_and_derivatives(self):
"vjps": [5, 6],
"vjp_batches": [1],
"execute_and_vjp_batches": [1],
- "resources": [Resources(num_wires=1)] * 12,
+ "resources": [
+ SpecsResources(
+ num_allocs=1,
+ gate_types={},
+ gate_sizes={},
+ measurements={"expval(PauliZ)": 1},
+ depth=0,
+ )
+ ]
+ * 12,
"errors": [{}] * 12,
}
@@ -121,11 +158,11 @@ def test_tracking_resources(self):
[qml.expval(qml.PauliZ(1)), qml.expval(qml.PauliY(2))],
)
- expected_resources = Resources(
- num_wires=3,
- num_gates=6,
+ expected_resources = SpecsResources(
+ num_allocs=3,
gate_types={"Hadamard": 3, "CNOT": 2, "RZ": 1},
gate_sizes={1: 4, 2: 2},
+ measurements={"expval(PauliZ)": 1, "expval(PauliY)": 1},
depth=3,
)
diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py
index 73f801253a7..5edd84045a2 100644
--- a/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py
+++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py
@@ -18,7 +18,7 @@
import pytest
import pennylane as qml
-from pennylane.resource import Resources
+from pennylane.resource import SpecsResources
class TestTracking:
@@ -47,12 +47,20 @@ def test_tracking(self):
with qml.Tracker(dev) as tracker:
dev.execute(qs)
+ res = SpecsResources(
+ gate_types={},
+ gate_sizes={},
+ measurements={"expval(GellMann)": 1},
+ depth=0,
+ num_allocs=1,
+ )
+
assert tracker.history == {
"batches": [1],
"executions": [1],
"simulations": [1],
"results": [1.0],
- "resources": [Resources(num_wires=1)],
+ "resources": [res],
"errors": [{}],
}
assert tracker.totals == {
@@ -65,7 +73,7 @@ def test_tracking(self):
"executions": 1,
"simulations": 1,
"results": 1,
- "resources": Resources(num_wires=1),
+ "resources": res,
"errors": {},
}
@@ -83,11 +91,11 @@ def test_tracking_resources(self):
[qml.expval(qml.GellMann(1, 8)), qml.expval(qml.GellMann(2, 7))],
)
- expected_resources = Resources(
- num_wires=3,
- num_gates=6,
+ expected_resources = SpecsResources(
gate_types={"THadamard": 3, "TAdd": 2, "TRZ": 1},
gate_sizes={1: 4, 2: 2},
+ measurements={"expval(GellMann)": 2},
+ num_allocs=3,
depth=3,
)
diff --git a/tests/devices/test_default_clifford.py b/tests/devices/test_default_clifford.py
index 5678315981a..426d87ed3ed 100644
--- a/tests/devices/test_default_clifford.py
+++ b/tests/devices/test_default_clifford.py
@@ -527,11 +527,11 @@ def test_tracker():
"results": 0.0,
}
assert np.allclose(tracker.history.pop("results")[0], 0.0)
- assert tracker.history.pop("resources")[0] == qml.resource.Resources(
- num_wires=2,
- num_gates=2,
+ assert tracker.history.pop("resources")[0] == qml.resource.SpecsResources(
+ num_allocs=2,
gate_types={"Hadamard": 1, "CNOT": 1},
gate_sizes={1: 1, 2: 1},
+ measurements={"expval(PauliZ)": 1},
depth=2,
)
assert tracker.history == {
diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py
index 8b3c1131415..017f39f4238 100644
--- a/tests/devices/test_null_qubit.py
+++ b/tests/devices/test_null_qubit.py
@@ -13,8 +13,6 @@
# limitations under the License.
"""Tests for null.qubit."""
-from collections import defaultdict as dd
-
import numpy as np
import pytest
@@ -175,11 +173,11 @@ def test_tracking():
"vjp_batches": [1],
"execute_and_vjp_batches": [1],
"resources": [
- qml.resource.Resources(
- num_wires=2,
- num_gates=2,
- gate_types=dd(int, {"Hadamard": 1, "FlipSign": 1}),
- gate_sizes=dd(int, {1: 1, 2: 1}),
+ qml.resource.SpecsResources(
+ num_allocs=2,
+ gate_types={"Hadamard": 1, "FlipSign": 1},
+ gate_sizes={1: 1, 2: 1},
+ measurements={"expval(PauliZ)": 1},
depth=2,
)
]
diff --git a/tests/devices/test_qubit_device.py b/tests/devices/test_qubit_device.py
index c7c9d2661ff..157de67fd1d 100644
--- a/tests/devices/test_qubit_device.py
+++ b/tests/devices/test_qubit_device.py
@@ -30,11 +30,10 @@
MeasurementProcess,
ProbabilityMP,
SampleMP,
- Shots,
StateMP,
VarianceMP,
)
-from pennylane.resource import Resources
+from pennylane.resource import SpecsResources
from pennylane.tape import QuantumScript
from pennylane.wires import Wires
@@ -1494,10 +1493,28 @@ class TestResourcesTracker:
)
expected_resources = (
- Resources(2, 2, {"Hadamard": 1, "CNOT": 1}, {1: 1, 2: 1}, 2, Shots(None)),
- Resources(3, 3, {"PauliZ": 1, "CNOT": 1, "RX": 1}, {1: 2, 2: 1}, 2, Shots(10)),
- Resources(2, 6, {"Hadamard": 3, "RX": 2, "CNOT": 1}, {1: 5, 2: 1}, 4, Shots((10, 10, 50))),
- ) # Resources(wires, gates, gate_types, gate_sizes, depth, shots)
+ SpecsResources(
+ num_allocs=2,
+ gate_types={"Hadamard": 1, "CNOT": 1},
+ gate_sizes={1: 1, 2: 1},
+ measurements={},
+ depth=2,
+ ),
+ SpecsResources(
+ num_allocs=3,
+ gate_types={"PauliZ": 1, "CNOT": 1, "RX": 1},
+ gate_sizes={1: 2, 2: 1},
+ measurements={},
+ depth=2,
+ ),
+ SpecsResources(
+ num_allocs=2,
+ gate_types={"Hadamard": 3, "RX": 2, "CNOT": 1},
+ gate_sizes={1: 5, 2: 1},
+ measurements={},
+ depth=4,
+ ),
+ )
@pytest.mark.all_interfaces
@pytest.mark.parametrize(
@@ -1523,8 +1540,20 @@ def test_tracker_multi_execution(self):
qs1 = qml.tape.QuantumScript([qml.Hadamard(0), qml.CNOT([0, 1])])
qs2 = qml.tape.QuantumScript([qml.PauliZ(0), qml.CNOT([0, 1]), qml.RX(1.23, 2)])
- exp_res1 = Resources(2, 2, {"Hadamard": 1, "CNOT": 1}, {1: 1, 2: 1}, 2, Shots(10))
- exp_res2 = Resources(3, 3, {"PauliZ": 1, "CNOT": 1, "RX": 1}, {1: 2, 2: 1}, 2, Shots(10))
+ exp_res1 = SpecsResources(
+ num_allocs=2,
+ gate_types={"Hadamard": 1, "CNOT": 1},
+ gate_sizes={1: 1, 2: 1},
+ measurements={},
+ depth=2,
+ )
+ exp_res2 = SpecsResources(
+ num_allocs=3,
+ gate_types={"PauliZ": 1, "CNOT": 1, "RX": 1},
+ gate_sizes={1: 2, 2: 1},
+ measurements={},
+ depth=2,
+ )
dev = DefaultQubitLegacy(shots=10, wires=[0, 1, 2])
with qml.Tracker(dev) as tracker:
@@ -1550,12 +1579,11 @@ def circuit(x):
return qml.expval(qml.PauliZ(0))
x = pnp.array(0.1, requires_grad=True)
- expected_resources = Resources(
- num_wires=1,
- num_gates=1,
+ expected_resources = SpecsResources(
+ num_allocs=1,
gate_types={"RX": 1},
gate_sizes={1: 1},
- shots=Shots(100),
+ measurements={"expval(PauliZ)": 1},
depth=1,
)
diff --git a/tests/ops/test_meta.py b/tests/ops/test_meta.py
index 299a565e314..2aec2ba6495 100644
--- a/tests/ops/test_meta.py
+++ b/tests/ops/test_meta.py
@@ -58,9 +58,8 @@ def qfunc():
dev = qml.device("default.qubit", wires=3)
optimized_qfunc = qml.compile(qfunc)
optimized_qnode = qml.QNode(optimized_qfunc, dev)
- optimized_gates = qml.specs(optimized_qnode)()["resources"].gate_sizes[1]
- assert optimized_gates == 0
+ assert 1 not in qml.specs(optimized_qnode)()["resources"].gate_sizes
def test_barrier_edge_cases(self):
r"""Test that the barrier works in edge cases."""
@@ -80,9 +79,7 @@ def qfunc():
optimized_qfunc = qml.compile(qfunc)
optimized_qnode = qml.QNode(optimized_qfunc, dev)
- optimized_gates = qml.specs(optimized_qnode)()["resources"].gate_sizes[1]
-
- assert optimized_gates == 0
+ assert 1 not in qml.specs(optimized_qnode)()["resources"].gate_sizes
def qfunc1():
qml.Hadamard(wires=0)
diff --git a/tests/qnn/test_qnn_torch.py b/tests/qnn/test_qnn_torch.py
index 0d56d1facba..3d2e45a701c 100644
--- a/tests/qnn/test_qnn_torch.py
+++ b/tests/qnn/test_qnn_torch.py
@@ -15,7 +15,6 @@
Tests for the pennylane.qnn.torch module.
"""
import math
-from collections import defaultdict
from unittest import mock
import numpy as np
@@ -948,16 +947,16 @@ def circuit(inputs, w1, w2):
info = qml.specs(qlayer)(x)
- gate_sizes = defaultdict(int, {1: 1, 2: 2})
- gate_types = defaultdict(int, {"AngleEmbedding": 1, "RX": 1, "StronglyEntanglingLayers": 1})
- expected_resources = qml.resource.Resources(
- num_wires=2, num_gates=3, gate_types=gate_types, gate_sizes=gate_sizes, depth=3
+ gate_sizes = {1: 1, 2: 2}
+ gate_types = {"AngleEmbedding": 1, "RX": 1, "StronglyEntanglingLayers": 1}
+ expected_resources = qml.resource.SpecsResources(
+ num_allocs=2,
+ gate_types=gate_types,
+ gate_sizes=gate_sizes,
+ measurements={"expval(PauliZ)": 2},
+ depth=3,
)
assert info["resources"] == expected_resources
- assert info["num_observables"] == 2
assert info["num_device_wires"] == 3
- assert info["num_tape_wires"] == 2
- assert info["num_trainable_params"] == 2
- assert info["interface"] == "torch"
assert info["device_name"] == "default.qubit"
diff --git a/tests/resource/test_error/test_error.py b/tests/resource/test_error/test_error.py
index c79c1105aae..06a99675d82 100644
--- a/tests/resource/test_error/test_error.py
+++ b/tests/resource/test_error/test_error.py
@@ -277,16 +277,6 @@ def test_computation(self):
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
-
def test_tracker(self):
"""Test that tracker are tracking errors as expected."""
diff --git a/tests/resource/test_resource.py b/tests/resource/test_resource.py
index 2276b793b7d..023fd858613 100644
--- a/tests/resource/test_resource.py
+++ b/tests/resource/test_resource.py
@@ -26,8 +26,10 @@
from pennylane.measurements import Shots
from pennylane.operation import Operation
from pennylane.resource.resource import (
+ CircuitSpecs,
Resources,
ResourcesOperation,
+ SpecsResources,
_combine_dict,
_count_resources,
_scale_dict,
@@ -35,7 +37,7 @@
add_in_series,
mul_in_parallel,
mul_in_series,
- specs_from_tape,
+ resources_from_tape,
substitute,
)
from pennylane.tape import QuantumScript
@@ -613,95 +615,6 @@ def resources(self):
assert CustomOPWithResources(wires=[0, 1]) # shouldn't raise an error
-class _CustomOpWithResource(ResourcesOperation): # pylint: disable=too-few-public-methods
- num_wires = 2
- name = "CustomOp1"
-
- def resources(self):
- return Resources(
- num_wires=self.num_wires,
- num_gates=3,
- gate_types={"Identity": 1, "PauliZ": 2},
- gate_sizes={1: 3},
- depth=3,
- )
-
-
-class _CustomOpWithoutResource(Operation): # pylint: disable=too-few-public-methods
- num_wires = 2
- name = "CustomOp2"
-
-
-lst_ops_and_shots = (
- ([], Shots(None)),
- ([qml.Hadamard(0), qml.CNOT([0, 1])], Shots(None)),
- ([qml.PauliZ(0), qml.CNOT([0, 1]), qml.RX(1.23, 2)], Shots(10)),
- (
- [
- qml.Hadamard(0),
- qml.RX(1.23, 1),
- qml.CNOT([0, 1]),
- qml.RX(4.56, 1),
- qml.Hadamard(0),
- qml.Hadamard(1),
- ],
- Shots(100),
- ),
- ([qml.Hadamard(0), qml.CNOT([0, 1]), _CustomOpWithResource(wires=[1, 0])], Shots(None)),
- (
- [
- qml.PauliZ(0),
- qml.CNOT([0, 1]),
- qml.RX(1.23, 2),
- _CustomOpWithResource(wires=[0, 2]),
- _CustomOpWithoutResource(wires=[0, 1]),
- ],
- Shots((10, (50, 2))),
- ),
- (
- [
- qml.Hadamard(0),
- qml.RX(1.23, 1),
- qml.CNOT([0, 1]),
- qml.RX(4.56, 1),
- qml.Hadamard(0),
- qml.Hadamard(1),
- _CustomOpWithoutResource(wires=[0, 1]),
- ],
- Shots(100),
- ),
-)
-
-resources_data = (
- Resources(),
- Resources(2, 2, {"Hadamard": 1, "CNOT": 1}, {1: 1, 2: 1}, 2),
- Resources(3, 3, {"PauliZ": 1, "CNOT": 1, "RX": 1}, {1: 2, 2: 1}, 2, Shots(10)),
- Resources(2, 6, {"Hadamard": 3, "RX": 2, "CNOT": 1}, {1: 5, 2: 1}, 4, Shots(100)),
- Resources(2, 5, {"Hadamard": 1, "CNOT": 1, "Identity": 1, "PauliZ": 2}, {1: 4, 2: 1}, 5),
- Resources(
- 3,
- 7,
- {"PauliZ": 3, "CNOT": 1, "RX": 1, "Identity": 1, "CustomOp2": 1},
- {1: 5, 2: 2},
- 6,
- Shots((10, (50, 2))),
- ),
- Resources(
- 2, 7, {"Hadamard": 3, "RX": 2, "CNOT": 1, "CustomOp2": 1}, {1: 5, 2: 2}, 5, Shots(100)
- ),
-) # Resources(wires, gates, gate_types, gate_sizes, depth, shots)
-
-
-@pytest.mark.parametrize(
- "ops_and_shots, expected_resources", zip(lst_ops_and_shots, resources_data)
-)
-def test_count_resources(ops_and_shots, expected_resources):
- """Test the count resources method."""
- ops, shots = ops_and_shots
- computed_resources = _count_resources(QuantumScript(ops=ops, shots=shots))
- assert computed_resources == expected_resources
-
-
def test_combine_dict():
"""Test that we can combine dictionaries as expected."""
d1 = defaultdict(int, {"a": 2, "b": 4, "c": 6})
@@ -726,7 +639,7 @@ def test_scale_dict(scalar):
@pytest.mark.parametrize("compute_depth", (True, False))
def test_specs_compute_depth(compute_depth):
- """Test that depth is skipped with `specs_from_tape`."""
+ """Test that depth is skipped with `resources_from_tape`."""
ops = [
qml.RX(0.432, wires=0),
@@ -737,7 +650,470 @@ def test_specs_compute_depth(compute_depth):
obs = [qml.expval(qml.PauliX(wires="a")), qml.probs(wires=[0, "a"])]
tape = QuantumScript(ops=ops, measurements=obs)
- specs = specs_from_tape(tape, compute_depth=compute_depth)
+ resources = resources_from_tape(tape, compute_depth=compute_depth)
+
+ assert resources.depth == (3 if compute_depth else None)
+
+
+###########################################################################
+## Tests for specs dataclasses
+###########################################################################
+
+
+class TestSpecsResources:
+ """Test the methods and attributes of the SpecsResource class"""
+
+ def example_specs_resource(self):
+ """Generate an example SpecsResources instance."""
+ return SpecsResources(
+ gate_types={"Hadamard": 2, "CNOT": 1},
+ gate_sizes={1: 2, 2: 1},
+ measurements={"expval(PauliZ)": 1},
+ num_allocs=2,
+ depth=2,
+ )
+
+ def test_depth_autoassign(self):
+ """Test that the SpecsResources class auto-assigns depth as None if not provided."""
+
+ s = SpecsResources(
+ gate_types={"Hadamard": 2, "CNOT": 1},
+ gate_sizes={1: 2, 2: 1},
+ measurements={"expval(PauliZ)": 1},
+ num_allocs=2,
+ )
+
+ assert s.depth is None
+
+ def test_num_gates(self):
+ """Test that the SpecsResources class handles `num_gates` as expected."""
+
+ with pytest.raises(
+ ValueError,
+ match="Inconsistent gate counts: `gate_types` and `gate_sizes` describe different amounts of gates.",
+ ):
+ # Gate counts don't match
+ _ = SpecsResources(
+ gate_types={"Hadamard": 1}, gate_sizes={1: 2}, measurements={}, num_allocs=0
+ )
+
+ s = self.example_specs_resource()
+
+ assert s.num_gates == 3
+
+ def test_immutable(self):
+ """Test that SpecsResources is immutable."""
+
+ s = self.example_specs_resource()
+
+ with pytest.raises(FrozenInstanceError, match="cannot assign to field"):
+ s.gate_types = {}
+
+ with pytest.raises(FrozenInstanceError, match="cannot assign to field"):
+ s.gate_sizes = {}
+
+ with pytest.raises(FrozenInstanceError, match="cannot assign to field"):
+ s.measurements = {}
+
+ with pytest.raises(FrozenInstanceError, match="cannot assign to field"):
+ s.num_allocs = 1
+
+ with pytest.raises(FrozenInstanceError, match="cannot assign to field"):
+ s.depth = 0
+
+ def test_getitem(self):
+ """Test that SpecsResources supports indexing via __getitem__."""
+
+ s = self.example_specs_resource()
+
+ assert s["gate_types"] == s.gate_types
+ assert s["gate_sizes"] == s.gate_sizes
+ assert s["measurements"] == s.measurements
+ assert s["num_allocs"] == s.num_allocs
+ assert s["depth"] == s.depth
+
+ assert s["num_gates"] == s.num_gates
+
+ # Check removed keys
+ with pytest.raises(
+ KeyError,
+ match="shots is no longer included within specs's resources, check the top-level object instead.",
+ ):
+ _ = s["shots"]
+ with pytest.raises(
+ KeyError,
+ match="num_wires has been renamed to num_allocs to more accurate describe what it measures.",
+ ):
+ _ = s["num_wires"]
+
+ # Try nonexistent key
+ with pytest.raises(
+ KeyError,
+ match="key 'potato' not available. Options are ",
+ ):
+ _ = s["potato"]
+
+ def test_str(self):
+ """Test the string representation of a SpecsResources instance."""
+
+ s = self.example_specs_resource()
+
+ expected = "Total qubit allocations: 2\n"
+ expected += "Total gates: 3\n"
+ expected += "Circuit depth: 2\n"
+ expected += "\n"
+ expected += "Gate types:\n"
+ expected += " Hadamard: 2\n"
+ expected += " CNOT: 1\n"
+ expected += "\n"
+ expected += "Measurements:\n"
+ expected += " expval(PauliZ): 1"
+
+ expected_indented = (" " + expected.replace("\n", "\n ")).replace("\n \n", "\n\n")
+
+ assert str(s) == expected
+ assert s.to_pretty_str() == expected
+ assert s.to_pretty_str(preindent=4) == expected_indented
+
+ # Check with no depth, gates, or measurements
+
+ s = SpecsResources(gate_types={}, gate_sizes={}, measurements={}, num_allocs=0)
+
+ expected = "Total qubit allocations: 0\n"
+ expected += "Total gates: 0\n"
+ expected += "Circuit depth: Not computed\n"
+ expected += "\n"
+ expected += "Gate types:\n"
+ expected += " No gates.\n"
+ expected += "\n"
+ expected += "Measurements:\n"
+ expected += " No measurements."
+
+ expected_indented = (" " + expected.replace("\n", "\n ")).replace("\n \n", "\n\n")
+
+ assert str(s) == expected
+ assert s.to_pretty_str() == expected
+ assert s.to_pretty_str(preindent=4) == expected_indented
+
+ def test_to_dict(self):
+ """Test the to_dict method of SpecsResources."""
+
+ s = self.example_specs_resource()
+
+ expected = {
+ "gate_types": {"Hadamard": 2, "CNOT": 1},
+ "gate_sizes": {1: 2, 2: 1},
+ "measurements": {"expval(PauliZ)": 1},
+ "num_allocs": 2,
+ "depth": 2,
+ "num_gates": 3,
+ }
+
+ assert s.to_dict() == expected
+
+
+class TestCircuitSpecs:
+
+ def example_specs_result(self):
+ """Generate an example CircuitSpecs instance."""
+ return CircuitSpecs(
+ device_name="default.qubit",
+ num_device_wires=5,
+ shots=Shots(1000),
+ level=2,
+ resources=SpecsResources(
+ gate_types={"Hadamard": 2, "CNOT": 1},
+ gate_sizes={1: 2, 2: 1},
+ measurements={"expval(PauliZ)": 1},
+ num_allocs=2,
+ depth=2,
+ ),
+ )
+
+ def example_specs_result_multi(self):
+ """Generate an example CircuitSpecs instance with multiple levels and batches."""
+ return CircuitSpecs(
+ device_name="default.qubit",
+ num_device_wires=5,
+ shots=Shots(1000),
+ level=[1, 2],
+ resources={
+ 1: SpecsResources(
+ gate_types={"Hadamard": 4, "CNOT": 2},
+ gate_sizes={1: 4, 2: 2},
+ measurements={"expval(PauliX)": 1, "expval(PauliZ)": 1},
+ num_allocs=2,
+ depth=2,
+ ),
+ 3: [
+ SpecsResources(
+ gate_types={"CNOT": 1},
+ gate_sizes={2: 1},
+ measurements={"expval(PauliX)": 1},
+ num_allocs=2,
+ depth=1,
+ ),
+ SpecsResources(
+ gate_types={"CNOT": 1},
+ gate_sizes={2: 1},
+ measurements={"expval(PauliZ)": 1},
+ num_allocs=2,
+ depth=1,
+ ),
+ ],
+ },
+ )
+
+ def test_blank_init(self):
+ """Test that CircuitSpecss can be instantiated with no arguments."""
+ r = CircuitSpecs() # should not raise any errors
+
+ assert r.device_name is None
+ assert r.num_device_wires is None
+ assert r.shots is None
+ assert r.level is None
+ assert r.resources is None
+
+ def test_getitem(self):
+ """Test that CircuitSpecs supports indexing via __getitem__."""
+
+ r = self.example_specs_result()
+
+ assert r["device_name"] == r.device_name
+ assert r["num_device_wires"] == r.num_device_wires
+ assert r["shots"] == r.shots
+ assert r["level"] == r.level
+ assert r["resources"] == r.resources
+
+ def test_getitem_removed_keys(self):
+ """Test that CircuitSpecs raises more descriptive KeyErrors for removed keys."""
+
+ r = self.example_specs_result()
+
+ with pytest.raises(
+ KeyError,
+ match="num_observables is no longer in top-level specs and has instead been absorbed into the 'measurements' attribute of the specs's resources.",
+ ):
+ _ = r["num_observables"]
+
+ for key in ("interface", "diff_method", "errors", "num_tape_wires"):
+ with pytest.raises(
+ KeyError,
+ match=f"key '{key}' is no longer included in specs.",
+ ):
+ _ = r[key]
+
+ for key in (
+ "gradient_fn",
+ "gradient_options",
+ "num_gradient_executions",
+ "num_trainable_params",
+ ):
+ with pytest.raises(
+ KeyError,
+ match=f"key '{key}' is no longer included in specs, as specs no longer gathers gradient information.",
+ ):
+ _ = r[key]
+
+ # Check nonexistent key
+ with pytest.raises(
+ KeyError,
+ match="key 'potato' not available. Options are ",
+ ):
+ _ = r["potato"]
+
+ def test_to_dict(self):
+ """Test the to_dict method of CircuitSpecs."""
+
+ r = self.example_specs_result()
+
+ expected = {
+ "device_name": "default.qubit",
+ "num_device_wires": 5,
+ "shots": Shots(1000),
+ "level": 2,
+ "resources": {
+ "gate_types": {"Hadamard": 2, "CNOT": 1},
+ "gate_sizes": {1: 2, 2: 1},
+ "measurements": {"expval(PauliZ)": 1},
+ "num_allocs": 2,
+ "depth": 2,
+ "num_gates": 3,
+ },
+ }
+
+ assert r.to_dict() == expected
+
+ r = self.example_specs_result_multi()
+
+ expected = {
+ "device_name": "default.qubit",
+ "num_device_wires": 5,
+ "shots": Shots(1000),
+ "level": [1, 2],
+ "resources": {
+ 1: {
+ "gate_types": {"Hadamard": 4, "CNOT": 2},
+ "gate_sizes": {1: 4, 2: 2},
+ "measurements": {"expval(PauliX)": 1, "expval(PauliZ)": 1},
+ "num_allocs": 2,
+ "depth": 2,
+ "num_gates": 6,
+ },
+ 3: [
+ {
+ "gate_types": {"CNOT": 1},
+ "gate_sizes": {2: 1},
+ "measurements": {"expval(PauliX)": 1},
+ "num_allocs": 2,
+ "depth": 1,
+ "num_gates": 1,
+ },
+ {
+ "gate_types": {"CNOT": 1},
+ "gate_sizes": {2: 1},
+ "measurements": {"expval(PauliZ)": 1},
+ "num_allocs": 2,
+ "depth": 1,
+ "num_gates": 1,
+ },
+ ],
+ },
+ }
+
+ assert r.to_dict() == expected
+
+ def test_str(self):
+ """Test the string representation of a CircuitSpecs instance."""
+
+ r = self.example_specs_result()
+
+ expected = "Device: default.qubit\n"
+ expected += "Device wires: 5\n"
+ expected += "Shots: Shots(total=1000)\n"
+ expected += "Level: 2\n"
+ expected += "\n"
+ expected += "Resource specifications:\n"
+ expected += r.resources.to_pretty_str(preindent=2)
+
+ assert str(r) == expected
+
+ def test_str_multi(self):
+ """Test the string representation of a CircuitSpecs instance."""
+
+ r = self.example_specs_result_multi()
+
+ expected = "Device: default.qubit\n"
+ expected += "Device wires: 5\n"
+ expected += "Shots: Shots(total=1000)\n"
+ expected += "Level: [1, 2]\n"
+ expected += "\n"
+ expected += "Resource specifications:\n"
+
+ expected += "Level = 1:\n"
+ expected += r.resources[1].to_pretty_str(preindent=2)
+
+ expected += "\n\n" + "-" * 60 + "\n\n"
+
+ expected += "Level = 3:\n"
+ expected += " Batched tape 0:\n"
+ expected += r.resources[3][0].to_pretty_str(preindent=4)
+ expected += "\n\n Batched tape 1:\n"
+ expected += r.resources[3][1].to_pretty_str(preindent=4)
+
+ assert str(r) == expected
+
+
+class TestCountResources:
+ class _CustomOpWithResource(ResourcesOperation): # pylint: disable=too-few-public-methods
+ num_wires = 2
+ name = "CustomOp1"
+
+ def resources(self):
+ return Resources(
+ num_wires=self.num_wires,
+ num_gates=3,
+ gate_types={"Identity": 1, "PauliZ": 2},
+ gate_sizes={1: 3},
+ depth=3,
+ )
+
+ class _CustomOpWithoutResource(Operation): # pylint: disable=too-few-public-methods
+ num_wires = 2
+ name = "CustomOp2"
+
+ scripts = (
+ QuantumScript(ops=[], measurements=[]),
+ QuantumScript(
+ ops=[qml.Hadamard(0), qml.CNOT([0, 1])], measurements=[qml.expval(qml.PauliZ(0))]
+ ),
+ QuantumScript(
+ ops=[qml.PauliZ(0), qml.CNOT([0, 1]), qml.RX(1.23, 2)],
+ measurements=[qml.expval(qml.exp(qml.PauliZ(0)))],
+ shots=Shots(10),
+ ),
+ QuantumScript(
+ ops=[
+ qml.PauliZ(0),
+ qml.CNOT([0, 1]),
+ qml.RX(1.23, 2),
+ _CustomOpWithResource(wires=[0, 2]),
+ _CustomOpWithoutResource(wires=[0, 1]),
+ ],
+ measurements=[qml.probs()],
+ ),
+ QuantumScript(
+ ops=[
+ qml.ctrl(op=qml.IsingXX(0.5, wires=[10, 11]), control=range(10)),
+ qml.ctrl(op=qml.IsingXX(0.5, wires=[10, 11]), control=range(5)),
+ qml.ctrl(op=qml.IsingXX(0.5, wires=[10, 11]), control=[0]),
+ qml.CNOT([0, 1]),
+ qml.Toffoli([0, 1, 2]),
+ qml.ctrl(op=qml.PauliX(10), control=[0]),
+ qml.ctrl(op=qml.PauliX(10), control=[0, 1]),
+ ],
+ measurements=[qml.probs()],
+ ),
+ )
+
+ expected_resources = (
+ SpecsResources({}, {}, {}, 0, 0),
+ SpecsResources({"Hadamard": 1, "CNOT": 1}, {1: 1, 2: 1}, {"expval(PauliZ)": 1}, 2, 2),
+ SpecsResources(
+ {"PauliZ": 1, "CNOT": 1, "RX": 1}, {1: 2, 2: 1}, {"expval(Exp(PauliZ))": 1}, 3, 2
+ ),
+ SpecsResources(
+ {"PauliZ": 3, "CNOT": 1, "RX": 1, "Identity": 1, "CustomOp2": 1},
+ {1: 5, 2: 2},
+ {"probs(all wires)": 1},
+ 3,
+ 6,
+ ),
+ SpecsResources(
+ {"10C(IsingXX)": 1, "5C(IsingXX)": 1, "C(IsingXX)": 1, "CNOT": 2, "Toffoli": 2},
+ {12: 1, 7: 1, 3: 3, 2: 2},
+ {"probs(all wires)": 1},
+ 12,
+ 7,
+ ),
+ ) # SpecsResources(gate_types, gate_sizes, measurements, num_allocs, depth)
+
+ @pytest.mark.parametrize("script, expected_resources", zip(scripts, expected_resources))
+ def test_count_resources(self, script, expected_resources):
+ """Test the count resources method."""
+ computed_resources = _count_resources(script)
+ assert computed_resources == expected_resources
+
+ @pytest.mark.parametrize("script, expected_resources", zip(scripts, expected_resources))
+ def test_count_resources_no_depth(self, script, expected_resources):
+ """Test the count resources method with depth disabled."""
+
+ computed_resources = _count_resources(script, compute_depth=False)
+ expected_resources = SpecsResources(
+ gate_types=expected_resources.gate_types,
+ gate_sizes=expected_resources.gate_sizes,
+ measurements=expected_resources.measurements,
+ num_allocs=expected_resources.num_allocs,
+ )
- assert len(specs) == 4
- assert specs["resources"].depth == (3 if compute_depth else None)
+ assert computed_resources == expected_resources
diff --git a/tests/resource/test_specs.py b/tests/resource/test_specs.py
index 1aa5f61dc07..21840d8dac0 100644
--- a/tests/resource/test_specs.py
+++ b/tests/resource/test_specs.py
@@ -13,24 +13,24 @@
# limitations under the License.
"""Unit tests for the specs transform"""
-# pylint: disable=invalid-sequence-index
-from collections import defaultdict
-from contextlib import nullcontext
+from functools import partial
+# pylint: disable=invalid-sequence-index
import pytest
import pennylane as qml
from pennylane import numpy as pnp
-from pennylane.tape import QuantumScript, QuantumScriptBatch
-from pennylane.typing import PostprocessingFn
+from pennylane.measurements import Shots
+from pennylane.resource import SpecsResources
devices_list = [
- (qml.device("default.qubit"), 1),
+ (qml.device("default.qubit"), None),
(qml.device("default.qubit", wires=2), 2),
]
-def test_error_with_bad_key():
+@pytest.mark.parametrize("key", ["bad_value", 123, "num_observables", "gradient_fn", "interface"])
+def test_error_with_bad_key(key):
"""Test that a helpful error message is raised if key does not exist."""
@qml.qnode(qml.device("null.qubit"))
@@ -38,8 +38,8 @@ def c():
return qml.state()
out = qml.specs(c)()
- with pytest.raises(KeyError, match="Options are {"):
- _ = out["bad_value"]
+ with pytest.raises(KeyError):
+ _ = out[key]
@pytest.mark.usefixtures("enable_and_disable_graph_decomp")
@@ -67,18 +67,16 @@ def circuit(x):
return circuit
@pytest.mark.parametrize(
- "level,expected_gates,exptected_train_params",
- [(0, 6, 1), (1, 4, 3), (2, 3, 3), (3, 1, 1), ("device", 2, 2)],
+ "level,expected_gates",
+ [(0, 6), (1, 4), (2, 3), (3, 1), ("device", 2)],
)
- def test_int_specs_level(self, level, expected_gates, exptected_train_params):
+ def test_int_specs_level(self, level, expected_gates):
circ = self.sample_circuit()
specs = qml.specs(circ, level=level)(0.1)
assert specs["level"] == level
assert specs["resources"].num_gates == expected_gates
- assert specs["num_trainable_params"] == exptected_train_params
-
@pytest.mark.parametrize(
"level1,level2",
[
@@ -93,54 +91,37 @@ def test_int_specs_level(self, level, expected_gates, exptected_train_params):
def test_equivalent_levels(self, level1, level2):
circ = self.sample_circuit()
- specs1 = qml.specs(circ, level=level1)(0.1)
- specs2 = qml.specs(circ, level=level2)(0.1)
+ specs1 = qml.specs(circ, level=level1)(0.1).to_dict()
+ specs2 = qml.specs(circ, level=level2)(0.1).to_dict()
del specs1["level"]
del specs2["level"]
assert specs1 == specs2
- @pytest.mark.parametrize(
- "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)]
- )
- def test_empty(self, diff_method, len_info):
+ @pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift", "adjoint"])
+ def test_diff_methods(self, diff_method):
dev = qml.device("default.qubit", wires=1)
@qml.qnode(dev, diff_method=diff_method)
def circ():
- return qml.expval(qml.PauliZ(0))
+ pass
- with (
- pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters")
- if diff_method == "parameter-shift"
- else nullcontext()
- ):
- info_func = qml.specs(circ)
- info = info_func()
- assert len(info) == len_info
+ expected_resources = SpecsResources(
+ gate_types={}, gate_sizes={}, measurements={}, num_allocs=0, depth=0
+ )
- expected_resources = qml.resource.Resources(num_wires=1, gate_types=defaultdict(int))
+ info = qml.specs(circ)()
assert info["resources"] == expected_resources
- assert info["num_observables"] == 1
assert info["num_device_wires"] == 1
- assert info["diff_method"] == diff_method
- assert info["num_trainable_params"] == 0
assert info["device_name"] == dev.name
assert info["level"] == "gradient"
- if diff_method == "parameter-shift":
- assert info["num_gradient_executions"] == 0
- assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift"
-
- @pytest.mark.parametrize(
- "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)]
- )
- def test_specs(self, diff_method, len_info):
+ def test_specs(self):
"""Test the specs transforms works in standard situations"""
dev = qml.device("default.qubit", wires=4)
- @qml.qnode(dev, diff_method=diff_method)
+ @qml.qnode(dev)
def circuit(x, y, add_RY=True):
qml.RX(x[0], wires=0)
qml.Toffoli(wires=(0, 1, 2))
@@ -155,24 +136,21 @@ def circuit(x, y, add_RY=True):
info = qml.specs(circuit)(x, y, add_RY=False)
- assert len(info) == len_info
-
- gate_sizes = defaultdict(int, {1: 2, 3: 1, 2: 1})
- gate_types = defaultdict(int, {"RX": 1, "Toffoli": 1, "CRY": 1, "Rot": 1})
- expected_resources = qml.resource.Resources(
- num_wires=3, num_gates=4, gate_types=gate_types, gate_sizes=gate_sizes, depth=3
+ gate_sizes = {1: 2, 3: 1, 2: 1}
+ gate_types = {"RX": 1, "Toffoli": 1, "CRY": 1, "Rot": 1}
+ expected_resources = SpecsResources(
+ num_allocs=3,
+ gate_types=gate_types,
+ gate_sizes=gate_sizes,
+ measurements={"expval(PauliZ)": 1, "expval(PauliX)": 1},
+ depth=3,
)
assert info["resources"] == expected_resources
- assert info["num_observables"] == 2
assert info["num_device_wires"] == 4
- assert info["diff_method"] == diff_method
- assert info["num_trainable_params"] == 4
assert info["device_name"] == dev.name
assert info["level"] == "gradient"
-
- if diff_method == "parameter-shift":
- assert info["num_gradient_executions"] == 6
+ assert info["shots"] == Shots(None)
@pytest.mark.parametrize("compute_depth", [True, False])
def test_specs_compute_depth(self, compute_depth):
@@ -192,7 +170,7 @@ def circuit(x):
info = qml.specs(circuit, compute_depth=compute_depth)(x)
- assert info["resources"].depth == (6 if compute_depth else None)
+ assert info.resources.depth == (6 if compute_depth else None)
def test_compute_depth_with_condition(self):
"""Tests that the depth is correct when there is a Conditional."""
@@ -228,25 +206,74 @@ def circuit3():
assert qml.specs(circuit3)()["resources"].depth == 3
- @pytest.mark.parametrize(
- "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)]
- )
- def test_specs_state(self, diff_method, len_info):
+ def test_specs_state(self):
"""Test specs works when state returned"""
dev = qml.device("default.qubit", wires=2)
- @qml.qnode(dev, diff_method=diff_method)
+ @qml.qnode(dev)
def circuit():
return qml.state()
info = qml.specs(circuit)()
- assert len(info) == len_info
- assert info["resources"] == qml.resource.Resources(gate_types=defaultdict(int))
+ assert info.resources == SpecsResources(
+ gate_types={},
+ gate_sizes={},
+ measurements={"state(all wires)": 1},
+ num_allocs=0, # Nothing actually used in this circuit
+ depth=0,
+ )
- assert info["num_observables"] == 1
- assert info["level"] == "gradient"
+ assert info.level == "gradient"
+
+ def test_specs_mcm(self):
+ """Test specs works when MCMs are used"""
+
+ dev = qml.device("default.qubit", wires=1)
+
+ @qml.qnode(dev)
+ def circuit():
+ m0 = qml.measure(0)
+ return qml.sample(m0)
+
+ info = qml.specs(circuit)()
+
+ assert info.resources == SpecsResources(
+ gate_types={"MidMeasureMP": 1},
+ gate_sizes={1: 1},
+ measurements={"sample(mcm)": 1},
+ num_allocs=1,
+ depth=1,
+ )
+
+ assert info.level == "gradient"
+
+ def test_specs_hamiltonian(self):
+ """Test specs works when hamiltonian returned"""
+
+ dev = qml.device("default.qubit", wires=3)
+
+ @qml.qnode(dev)
+ def circuit(i: int):
+ coeffs = [0.2, -0.543]
+ obs = [qml.X(0) @ qml.Z(1), qml.Z(i) @ qml.Hadamard(2)]
+ ham1 = qml.ops.LinearCombination(coeffs, obs)
+ ham2 = qml.Hamiltonian([1.0], [qml.exp(1j * qml.Z(0) @ qml.Z(1))])
+ return qml.expval(ham1), qml.expval(ham2)
+
+ info = qml.specs(circuit)(0)
+
+ assert info.resources == SpecsResources(
+ gate_types={},
+ gate_sizes={},
+ measurements={
+ "expval(Hamiltonian(num_wires=3, num_terms=2))": 1,
+ "expval(Hamiltonian(num_wires=2, num_terms=1))": 1,
+ },
+ num_allocs=3,
+ depth=0,
+ )
def test_level_with_diagonalizing_gates(self):
"""Test that when diagonalizing gates includes gates that are decomposed in
@@ -304,64 +331,108 @@ def circuit(x):
qml.X(0)
return qml.expval(H)
- specs_instance = qml.specs(circuit, level=1)(pnp.array([1.23, -1]))
-
- assert isinstance(specs_instance, dict)
-
- specs_list = qml.specs(circuit, level=2)(pnp.array([1.23, -1]))
-
- assert len(specs_list) == len(H)
-
- assert specs_list[0]["num_device_wires"] == specs_list[0]["num_tape_wires"] == 2
- assert specs_list[1]["num_device_wires"] == specs_list[1]["num_tape_wires"] == 3
- assert specs_list[2]["num_device_wires"] == specs_list[1]["num_tape_wires"] == 3
-
- def make_qnode_and_params(self, seed):
- """Generates a qnode and params for use in other tests"""
- n_layers = 2
- n_wires = 5
-
- dev = qml.device("default.qubit", wires=n_wires)
-
- @qml.qnode(dev)
- def circuit(params):
- qml.BasicEntanglerLayers(params, wires=range(n_wires))
- return qml.expval(qml.PauliZ(0))
-
- params_shape = qml.BasicEntanglerLayers.shape(n_layers=n_layers, n_wires=n_wires)
- rng = pnp.random.default_rng(seed=seed)
- params = rng.standard_normal(params_shape) # pylint:disable=no-member
-
- return circuit, params
-
- def test_gradient_transform(self):
- """Test that a gradient transform is properly labelled"""
- dev = qml.device("default.qubit", wires=2)
-
- @qml.qnode(dev, diff_method=qml.gradients.param_shift)
- def circuit():
- return qml.probs(wires=0)
-
- with pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters"):
- info = qml.specs(circuit)()
- assert info["diff_method"] == "pennylane.gradients.parameter_shift.param_shift"
- assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift"
-
- def test_custom_gradient_transform(self):
- """Test that a custom gradient transform is properly labelled"""
- dev = qml.device("default.qubit", wires=2)
-
- @qml.transform
- def my_transform(tape: QuantumScript) -> tuple[QuantumScriptBatch, PostprocessingFn]:
- return tape, None
-
- @qml.qnode(dev, diff_method=my_transform)
- def circuit():
- return qml.probs(wires=0)
-
- info = qml.specs(circuit)()
- assert info["diff_method"] == "test_specs.my_transform"
- assert info["gradient_fn"] == "test_specs.my_transform"
+ specs_output = qml.specs(circuit, level=1)(pnp.array([1.23, -1]))
+
+ # Check that there is only 1 output
+ assert isinstance(specs_output.resources, SpecsResources)
+
+ specs_output = qml.specs(circuit, level=2)(pnp.array([1.23, -1]))
+
+ assert isinstance(specs_output.resources, list)
+ assert len(specs_output.resources) == len(H)
+
+ assert specs_output.resources[0].num_allocs == 2
+ assert specs_output.resources[1].num_allocs == 3
+ assert specs_output.resources[2].num_allocs == 3
+
+ assert specs_output.level == 2
+ assert specs_output.device_name == "default.qubit"
+ assert specs_output.num_device_wires is None
+ assert specs_output.shots == Shots(None)
+
+ assert specs_output.to_dict() == {
+ "device_name": "default.qubit",
+ "num_device_wires": None,
+ "shots": Shots(None),
+ "level": 2,
+ "resources": [
+ {
+ "gate_types": {"RandomLayers": 1, "RX": 1, "SWAP": 1, "PauliX": 2},
+ "gate_sizes": {2: 2, 1: 3},
+ "measurements": {"expval(Prod(num_wires=2, num_terms=2))": 1},
+ "num_allocs": 2,
+ "depth": 5,
+ "num_gates": 5,
+ },
+ {
+ "gate_types": {"RandomLayers": 1, "RX": 1, "SWAP": 1, "PauliX": 2},
+ "gate_sizes": {2: 2, 1: 3},
+ "measurements": {"expval(Prod(num_wires=2, num_terms=2))": 1},
+ "num_allocs": 3,
+ "depth": 5,
+ "num_gates": 5,
+ },
+ {
+ "gate_types": {"RandomLayers": 1, "RX": 1, "SWAP": 1, "PauliX": 2},
+ "gate_sizes": {2: 2, 1: 3},
+ "measurements": {"expval(Prod(num_wires=2, num_terms=2))": 1},
+ "num_allocs": 3,
+ "depth": 5,
+ "num_gates": 5,
+ },
+ ],
+ }
+
+ assert (
+ str(specs_output)
+ == """Device: default.qubit
+Device wires: None
+Shots: Shots(total=None)
+Level: 2
+
+Resource specifications:
+ Batched tape 0:
+ Total qubit allocations: 2
+ Total gates: 5
+ Circuit depth: 5
+
+ Gate types:
+ RandomLayers: 1
+ RX: 1
+ SWAP: 1
+ PauliX: 2
+
+ Measurements:
+ expval(Prod(num_wires=2, num_terms=2)): 1
+
+ Batched tape 1:
+ Total qubit allocations: 3
+ Total gates: 5
+ Circuit depth: 5
+
+ Gate types:
+ RandomLayers: 1
+ RX: 1
+ SWAP: 1
+ PauliX: 2
+
+ Measurements:
+ expval(Prod(num_wires=2, num_terms=2)): 1
+
+ Batched tape 2:
+ Total qubit allocations: 3
+ Total gates: 5
+ Circuit depth: 5
+
+ Gate types:
+ RandomLayers: 1
+ RX: 1
+ SWAP: 1
+ PauliX: 2
+
+ Measurements:
+ expval(Prod(num_wires=2, num_terms=2)): 1"""
+ )
@pytest.mark.parametrize(
"device,num_wires",
@@ -378,28 +449,6 @@ def circuit():
info = qml.specs(circuit)()
assert info["num_device_wires"] == num_wires
- def test_no_error_contents_on_device_level(self):
- coeffs = [0.25, 0.75]
- ops = [qml.X(0), qml.Z(0)]
- H = qml.dot(coeffs, ops)
-
- @qml.qnode(qml.device("default.qubit"))
- def circuit():
- qml.Hadamard(0)
- qml.TrotterProduct(H, time=2.4, order=2)
-
- return qml.state()
-
- top_specs = qml.specs(circuit, level="top")()
- dev_specs = qml.specs(circuit, level="device")()
-
- assert "SpectralNormError" in top_specs["errors"]
- assert pnp.allclose(top_specs["errors"]["SpectralNormError"].error, 13.824)
-
- # At the device level, approximations don't exist anymore and therefore
- # we should expect an empty errors dictionary.
- assert dev_specs["errors"] == {}
-
def test_error_with_non_qnode(self):
"""Test that a helpful error message is raised if the input is not a QNode."""
@@ -411,6 +460,30 @@ def f():
):
qml.specs(f)()
+ def test_custom_level(self):
+ """Test that we can draw at a custom level."""
+
+ @qml.transforms.merge_rotations
+ @partial(qml.marker, level="my_level")
+ @qml.transforms.cancel_inverses
+ @qml.qnode(qml.device("null.qubit"))
+ def c():
+ qml.RX(0.2, 0)
+ qml.X(0)
+ qml.X(0)
+ qml.RX(0.2, 0)
+ return qml.state()
+
+ expected = SpecsResources(
+ num_allocs=1,
+ gate_types={"RX": 2},
+ gate_sizes={1: 2},
+ measurements={"state(all wires)": 1},
+ depth=2,
+ )
+
+ assert qml.specs(c, level="my_level")()["resources"] == expected
+
@pytest.mark.usefixtures("enable_graph_decomposition")
class TestSpecsGraphModeExclusive:
@@ -458,7 +531,7 @@ def circuit():
# Work wires calculation should be: device_wires - tape_wires
if num_device_wires:
assert specs["num_device_wires"] == num_device_wires
- assert specs["num_tape_wires"] == 1
+ assert specs["resources"].num_allocs == 1
# Check that the correct decomposition was used
assert expected_decomp in specs["resources"].gate_types
@@ -491,7 +564,7 @@ def circuit():
# Should report 1 work wire available (2 device wires - 1 tape wire)
assert specs["num_device_wires"] == 2
- assert specs["num_tape_wires"] == 1
+ assert specs["resources"].num_allocs == 1
# Fallback decomposition should be used (H gate)
assert "Hadamard" in specs["resources"].gate_types
@@ -510,4 +583,4 @@ def circuit():
# No work wires available (2 device wires - 2 tape wires = 0)
assert specs["num_device_wires"] == 2
- assert specs["num_tape_wires"] == 2
+ assert specs["resources"].num_allocs == 2
diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py
index 7046cccbdc4..f7ab514b7cb 100644
--- a/tests/tape/test_qscript.py
+++ b/tests/tape/test_qscript.py
@@ -13,7 +13,6 @@
# limitations under the License.
"""Unit tests for the QuantumScript"""
import copy
-from collections import defaultdict
import numpy as np
import pytest
@@ -590,15 +589,13 @@ def test_empty_qs_specs(self):
qs = QuantumScript()
assert qs._specs is None
- assert qs.specs["resources"] == qml.resource.Resources()
-
- assert qs.specs["num_observables"] == 0
- assert qs.specs["num_trainable_params"] == 0
-
- with pytest.raises(KeyError, match="is no longer in specs"):
- _ = qs.specs["num_diagonalizing_gates"]
-
- assert len(qs.specs) == 4
+ assert qs.specs["resources"] == qml.resource.SpecsResources(
+ num_allocs=0,
+ gate_types={},
+ gate_sizes={},
+ measurements={},
+ depth=0,
+ )
assert qs._specs is qs.specs
@@ -610,16 +607,16 @@ def test_specs_tape(self, make_script):
specs = qs.specs
assert qs._specs is specs
- assert len(specs) == 4
-
- gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})
- gate_sizes = defaultdict(int, {1: 3, 2: 1})
- expected_resources = qml.resource.Resources(
- num_wires=3, num_gates=4, gate_types=gate_types, gate_sizes=gate_sizes, depth=3
+ gate_types = {"RX": 2, "Rot": 1, "CNOT": 1}
+ gate_sizes = {1: 3, 2: 1}
+ expected_resources = qml.resource.SpecsResources(
+ num_allocs=3,
+ gate_types=gate_types,
+ gate_sizes=gate_sizes,
+ measurements={"expval(PauliX)": 1, "probs(2 wires)": 1},
+ depth=3,
)
assert specs["resources"] == expected_resources
- assert specs["num_observables"] == 2
- assert specs["num_trainable_params"] == 5
@pytest.mark.parametrize(
"shots, total_shots, shot_vector",
diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py
index 526dd4fa5ad..fcd388ca675 100644
--- a/tests/tape/test_tape.py
+++ b/tests/tape/test_tape.py
@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for the QuantumTape"""
+
# pylint: disable=protected-access,too-few-public-methods
import copy
-from collections import defaultdict
import numpy as np
import pytest
@@ -522,31 +522,29 @@ def test_specs_empty_tape(self, make_empty_tape):
"""Test specs attribute on an empty tape"""
tape = make_empty_tape
- gate_types = defaultdict(int)
- expected_resources = qml.resource.Resources(num_wires=2, gate_types=gate_types)
+ expected_resources = qml.resource.SpecsResources(
+ num_allocs=2,
+ gate_types={},
+ gate_sizes={},
+ measurements={"probs(all wires)": 1},
+ depth=0,
+ )
assert tape.specs["resources"] == expected_resources
- assert tape.specs["num_observables"] == 1
- assert tape.specs["num_trainable_params"] == 0
-
- assert len(tape.specs) == 4
-
def test_specs_tape(self, make_tape):
"""Tests that regular tapes return correct specifications"""
tape = make_tape
specs = tape.specs
- assert len(specs) == 4
-
- gate_sizes = defaultdict(int, {1: 3, 2: 1})
- gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})
- expected_resources = qml.resource.Resources(
- num_wires=3, num_gates=4, gate_types=gate_types, gate_sizes=gate_sizes, depth=3
+ expected_resources = qml.resource.SpecsResources(
+ num_allocs=3,
+ gate_types={"RX": 2, "Rot": 1, "CNOT": 1},
+ gate_sizes={1: 3, 2: 1},
+ measurements={"expval(PauliX)": 1, "probs(2 wires)": 1},
+ depth=3,
)
assert specs["resources"] == expected_resources
- assert specs["num_observables"] == 2
- assert specs["num_trainable_params"] == 5
def test_specs_add_to_tape(self, make_extendible_tape):
"""Test that tapes return correct specs after adding to them."""
@@ -554,18 +552,14 @@ def test_specs_add_to_tape(self, make_extendible_tape):
tape = make_extendible_tape
specs1 = tape.specs
- assert len(specs1) == 4
-
- gate_sizes = defaultdict(int, {1: 3, 2: 1})
- gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})
-
- expected_resoures = qml.resource.Resources(
- num_wires=3, num_gates=4, gate_types=gate_types, gate_sizes=gate_sizes, depth=3
+ expected_resources = qml.resource.SpecsResources(
+ num_allocs=3,
+ gate_types={"RX": 2, "Rot": 1, "CNOT": 1},
+ gate_sizes={1: 3, 2: 1},
+ measurements={},
+ depth=3,
)
- assert specs1["resources"] == expected_resoures
-
- assert specs1["num_observables"] == 0
- assert specs1["num_trainable_params"] == 5
+ assert specs1["resources"] == expected_resources
with tape as tape:
qml.CNOT(wires=[0, 1])
@@ -575,18 +569,14 @@ def test_specs_add_to_tape(self, make_extendible_tape):
specs2 = tape.specs
- assert len(specs2) == 4
-
- gate_sizes = defaultdict(int, {1: 4, 2: 2})
- gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 2, "RZ": 1})
-
- expected_resoures = qml.resource.Resources(
- num_wires=5, num_gates=6, gate_types=gate_types, gate_sizes=gate_sizes, depth=4
+ expected_resources = qml.resource.SpecsResources(
+ num_allocs=5,
+ gate_types={"RX": 2, "Rot": 1, "CNOT": 2, "RZ": 1},
+ gate_sizes={1: 4, 2: 2},
+ measurements={"expval(PauliX)": 1, "probs(2 wires)": 1},
+ depth=4,
)
- assert specs2["resources"] == expected_resoures
-
- assert specs2["num_observables"] == 2
- assert specs2["num_trainable_params"] == 6
+ assert specs2["resources"] == expected_resources
class TestParameters:
diff --git a/tests/templates/subroutines/time_evolution/test_trotter.py b/tests/templates/subroutines/time_evolution/test_trotter.py
index 466ffe04c5f..39ff86f53ef 100644
--- a/tests/templates/subroutines/time_evolution/test_trotter.py
+++ b/tests/templates/subroutines/time_evolution/test_trotter.py
@@ -25,7 +25,7 @@
from pennylane import numpy as qnp
from pennylane.math import allclose, get_interface
from pennylane.ops.functions.assert_valid import _test_decomposition_rule
-from pennylane.resource import Resources
+from pennylane.resource import Resources, SpecsResources
from pennylane.resource.error import SpectralNormError
from pennylane.templates.subroutines.time_evolution.trotter import (
TrotterizedQfunc,
@@ -717,11 +717,11 @@ def circ():
qml.TrotterProduct(hamiltonian, time, n=5, order=2)
return qml.expval(qml.Z(0))
- expected_resources = Resources(
- num_wires=2,
- num_gates=30,
- gate_types=defaultdict(int, {"Evolution": 30}),
- gate_sizes=defaultdict(int, {1: 30}),
+ expected_resources = SpecsResources(
+ num_allocs=2,
+ gate_types={"Evolution": 30},
+ gate_sizes={1: 30},
+ measurements={"expval(PauliZ)": 1},
depth=20,
)
@@ -734,38 +734,6 @@ def circ():
assert expected_resources == spec_resources
assert expected_resources == tracker_resources
- def test_resources_and_error(self):
- """Test that we can compute the resources and error together"""
- time = 0.1
- coeffs = qml.math.array([1.0, 0.5])
- hamiltonian = qml.dot(coeffs, [qml.X(0), qml.Y(0)])
-
- dev = qml.device("default.qubit")
-
- @qml.qnode(dev)
- def circ():
- qml.TrotterProduct(hamiltonian, time, n=2, order=2)
- return qml.expval(qml.Z(0))
-
- specs = qml.specs(circ)()
-
- computed_error = (specs["errors"])["SpectralNormError"]
- computed_resources = specs["resources"]
-
- # Expected resources and errors (computed by hand)
- expected_resources = Resources(
- num_wires=1,
- num_gates=8,
- gate_types=defaultdict(int, {"Evolution": 8}),
- gate_sizes=defaultdict(int, {1: 8}),
- depth=8,
- )
- expected_error = 0.001
-
- assert computed_resources == expected_resources
- assert isinstance(computed_error, SpectralNormError)
- assert qnp.isclose(computed_error.error, qml.math.array(expected_error))
-
class TestDecomposition:
"""Test the decomposition of the TrotterProduct class."""
diff --git a/tests/test_qnode.py b/tests/test_qnode.py
index c6c3c74332b..e300976809f 100644
--- a/tests/test_qnode.py
+++ b/tests/test_qnode.py
@@ -1534,7 +1534,7 @@ def circuit(x):
assert tracker.totals["executions"] == 1
assert tracker.history["resources"][0].gate_types["PauliX"] == 1
- assert tracker.history["resources"][0].gate_types["RX"] == 0
+ assert "RX" not in tracker.history["resources"][0].gate_types
def tet_transform_program_modifies_results(self):
"""Test integration with a transform that modifies the result output."""
diff --git a/tests/test_qnode_legacy.py b/tests/test_qnode_legacy.py
index 71c3e2130af..d828e279ec2 100644
--- a/tests/test_qnode_legacy.py
+++ b/tests/test_qnode_legacy.py
@@ -28,7 +28,7 @@
from pennylane import numpy as pnp
from pennylane import qnode
from pennylane.exceptions import PennyLaneDeprecationWarning, QuantumFunctionError
-from pennylane.resource import Resources
+from pennylane.resource import SpecsResources
from pennylane.tape import QuantumScript, QuantumScriptBatch
from pennylane.typing import PostprocessingFn
@@ -218,7 +218,15 @@ def circuit(params):
"shots": [None],
"batches": [1],
"batch_len": [1],
- "resources": [Resources(1, 1, {"RX": 1}, {1: 1}, 1)],
+ "resources": [
+ SpecsResources(
+ num_allocs=1,
+ gate_types={"RX": 1},
+ gate_sizes={1: 1},
+ measurements={"expval(PauliZ)": 1},
+ depth=1,
+ )
+ ],
}
def test_autograd_interface_device_switched_no_warnings(self):
@@ -1113,7 +1121,7 @@ def circuit(x):
assert tracker.totals["executions"] == 1
assert tracker.history["resources"][0].gate_types["PauliX"] == 1
- assert tracker.history["resources"][0].gate_types["RX"] == 0
+ assert "RX" not in tracker.history["resources"][0].gate_types
def tet_transform_program_modifies_results(self):
"""Test integration with a transform that modifies the result output."""
diff --git a/tests/test_vqe.py b/tests/test_vqe.py
index c5ef7ffc05f..179a749c393 100644
--- a/tests/test_vqe.py
+++ b/tests/test_vqe.py
@@ -973,7 +973,13 @@ def circuit():
res = qml.specs(circuit)()
- assert res["num_observables"] == 1
+ assert res["resources"] == qml.resource.SpecsResources(
+ num_allocs=2,
+ gate_types={"Hadamard": 1, "CNOT": 1},
+ gate_sizes={1: 1, 2: 1},
+ measurements={"expval(Hamiltonian(num_wires=2, num_terms=2))": 1},
+ depth=2,
+ )
class TestInterfaces:
diff --git a/tests/transforms/test_optimization/test_pattern_matching.py b/tests/transforms/test_optimization/test_pattern_matching.py
index cc967b1c1ef..541c894f974 100644
--- a/tests/transforms/test_optimization/test_pattern_matching.py
+++ b/tests/transforms/test_optimization/test_pattern_matching.py
@@ -275,8 +275,10 @@ def circuit():
optimized_qnode = qml.QNode(optimized_qfunc, dev)
optimized_qnode_res = optimized_qnode()
- toffolis_qnode = qml.specs(qnode)()["resources"].gate_types["Toffoli"]
- toffolis_optimized_qnode = qml.specs(optimized_qnode)()["resources"].gate_types["Toffoli"]
+ toffolis_qnode = qml.specs(qnode)()["resources"].gate_types.get("Toffoli", 0)
+ toffolis_optimized_qnode = qml.specs(optimized_qnode)()["resources"].gate_types.get(
+ "Toffoli", 0
+ )
tape = qml.workflow.construct_tape(qnode)()
assert len(tape.operations) == 11
@@ -323,12 +325,12 @@ def circuit():
optimized_qnode_res = optimized_qnode()
gate_qnode = qml.specs(qnode)()["resources"].gate_types
- swap_qnode = gate_qnode["SWAP"]
- cnot_qnode = gate_qnode["CNOT"]
+ swap_qnode = gate_qnode.get("SWAP", 0)
+ cnot_qnode = gate_qnode.get("CNOT", 0)
gate_qnode_optimized = qml.specs(optimized_qnode)()["resources"].gate_types
- swap_optimized_qnode = gate_qnode_optimized["SWAP"]
- cnot_optimized_qnode = gate_qnode_optimized["CNOT"]
+ swap_optimized_qnode = gate_qnode_optimized.get("SWAP", 0)
+ cnot_optimized_qnode = gate_qnode_optimized.get("CNOT", 0)
tape = qml.workflow.construct_tape(qnode)()
assert len(tape.operations) == 8
@@ -375,12 +377,12 @@ def circuit():
optimized_qnode_res = optimized_qnode()
gate_qnode = qml.specs(qnode)()["resources"].gate_types
- swap_qnode = gate_qnode["SWAP"]
- cnot_qnode = gate_qnode["CNOT"]
+ swap_qnode = gate_qnode.get("SWAP", 0)
+ cnot_qnode = gate_qnode.get("CNOT", 0)
gate_qnode_optimized = qml.specs(optimized_qnode)()["resources"].gate_types
- swap_optimized_qnode = gate_qnode_optimized["SWAP"]
- cnot_optimized_qnode = gate_qnode_optimized["CNOT"]
+ swap_optimized_qnode = gate_qnode_optimized.get("SWAP", 0)
+ cnot_optimized_qnode = gate_qnode_optimized.get("CNOT", 0)
tape = qml.workflow.construct_tape(qnode)()
assert len(tape.operations) == 11
@@ -427,12 +429,12 @@ def circuit():
optimized_qnode_res = optimized_qnode()
gate_qnode = qml.specs(qnode)()["resources"].gate_types
- cswap_qnode = gate_qnode["CSWAP"]
- cnot_qnode = gate_qnode["CNOT"]
+ cswap_qnode = gate_qnode.get("CSWAP", 0)
+ cnot_qnode = gate_qnode.get("CNOT", 0)
gate_qnode_optimized = qml.specs(optimized_qnode)()["resources"].gate_types
- cswap_optimized_qnode = gate_qnode_optimized["CSWAP"]
- cnot_optimized_qnode = gate_qnode_optimized["CNOT"]
+ cswap_optimized_qnode = gate_qnode_optimized.get("CSWAP", 0)
+ cnot_optimized_qnode = gate_qnode_optimized.get("CNOT", 0)
tape = qml.workflow.construct_tape(qnode)()
assert len(tape.operations) == 11
@@ -488,11 +490,15 @@ def circuit(x, y):
optimized_qnode = qml.QNode(optimized_qfunc, dev)
optimized_qnode_res = optimized_qnode(0.1, 0.2)
- rx_qnode = qml.specs(qnode)(0.1, 0.2)["resources"].gate_types["RX"]
- rx_optimized_qnode = qml.specs(optimized_qnode)(0.1, 0.2)["resources"].gate_types["RX"]
+ rx_qnode = qml.specs(qnode)(0.1, 0.2)["resources"].gate_types.get("RX", 0)
+ rx_optimized_qnode = qml.specs(optimized_qnode)(0.1, 0.2)["resources"].gate_types.get(
+ "RX", 0
+ )
- rz_qnode = qml.specs(qnode)(0.1, 0.2)["resources"].gate_types["RZ"]
- rz_optimized_qnode = qml.specs(optimized_qnode)(0.1, 0.2)["resources"].gate_types["RZ"]
+ rz_qnode = qml.specs(qnode)(0.1, 0.2)["resources"].gate_types.get("RZ", 0)
+ rz_optimized_qnode = qml.specs(optimized_qnode)(0.1, 0.2)["resources"].gate_types.get(
+ "RZ", 0
+ )
tape = qml.workflow.construct_tape(qnode)(0.1, 0.2)
assert len(tape.operations) == 14