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