Source code for pennylane.labs.resource_estimation.resources_base
# Copyright 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.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
r"""Base class for storing resources."""
from __future__ import annotations
import copy
from collections import defaultdict
from decimal import Decimal
from pennylane.labs.resource_estimation.qubit_manager import QubitManager
[docs]
class Resources:
r"""A container to track and update the resources used throughout a quantum circuit.
The resources tracked include number of gates, number of wires, and gate types.
Args:
qubit_manager (QubitManager): A qubit tracker class which contains the number of available
work wires, categorized as clean, dirty or algorithmic wires.
gate_types (dict): A dictionary storing operations (ResourceOperator) as keys and the number
of times they are used in the circuit (int) as values.
**Example**
The resources can be accessed as class attributes. Additionally, the :class:`~.Resources`
instance can be nicely displayed in the console.
>>> H = re.resource_rep(re.ResourceHadamard)
>>> X = re.resource_rep(re.ResourceX)
>>> Y = re.resource_rep(re.ResourceY)
>>> qm = re.QubitManager(work_wires=3)
>>> gt = defaultdict(int, {H: 10, X:7, Y:3})
>>>
>>> res = re.Resources(qubit_manager=qm, gate_types=gt)
>>> print(res)
--- Resources: ---
Total qubits: 3
Total gates : 20
Qubit breakdown:
clean qubits: 3, dirty qubits: 0, algorithmic qubits: 0
Gate breakdown:
{'Hadamard': 10, 'X': 7, 'Y': 3}
.. details::
:title: Usage Details
The :class:`Resources` object supports arithmetic operations which allow for quick addition
and multiplication of resources. When combining resources, we can make a simplifying
assumption about how they are applied in a quantum circuit (in series or in parallel).
When assuming the circuits were executed in series, the number of algorithmic qubits add
together. When assuming the circuits were executed in parallel, the maximum of each set of
algorithmic qubits is used. The clean auxiliary qubits can be reused between the circuits,
and thus we always use the maximum of each set when combining the resources. Finally, the
dirty qubits cannot be reused between circuits, thus we always add them together.
.. code-block::
from collections import defaultdict
# Resource reps for each operator:
H = re.resource_rep(re.ResourceHadamard)
X = re.resource_rep(re.ResourceX)
Z = re.resource_rep(re.ResourceZ)
CNOT = re.resource_rep(re.ResourceCNOT)
# state of qubits:
qm1 = re.QubitManager(work_wires={"clean":2, "dirty":1}, algo_wires=3)
qm2 = re.QubitManager(work_wires={"clean":1, "dirty":2}, algo_wires=4)
# state of gates:
gt1 = defaultdict(int, {H: 10, X:5, CNOT:2})
gt2 = defaultdict(int, {H: 15, Z:5, CNOT:4})
# resources:
res1 = re.Resources(qubit_manager=qm1, gate_types=gt1)
res2 = re.Resources(qubit_manager=qm2, gate_types=gt2)
.. code-block:: pycon
>>> print(res1)
--- Resources: ---
Total qubits: 6
Total gates : 17
Qubit breakdown:
clean qubits: 2, dirty qubits: 1, algorithmic qubits: 3
Gate breakdown:
{'Hadamard': 10, 'X': 5, 'CNOT': 2}
>>> print(res2)
--- Resources: ---
Total qubits: 7
Total gates : 24
Qubit breakdown:
clean qubits: 1, dirty qubits: 2, algorithmic qubits: 4
Gate breakdown:
{'Hadamard': 15, 'Z': 5, 'CNOT': 4}
Specifically, users can add together two instances of resources using the :code:`+` and
:code:`&` operators. These represent combining the resources assuming the circuits were
executed in series or parallel respectively.
.. code-block:: pycon
>>> res_in_series = res1 + res2
>>> print(res_in_series)
--- Resources: ---
Total qubits: 9
Total gates : 41
Qubit breakdown:
clean qubits: 2, dirty qubits: 3, algorithmic qubits: 4
Gate breakdown:
{'Hadamard': 25, 'X': 5, 'CNOT': 6, 'Z': 5}
>>> res_in_parallel = res1 & res2
>>> print(res_in_parallel)
--- Resources: ---
Total qubits: 12
Total gates : 41
Qubit breakdown:
clean qubits: 2, dirty qubits: 3, algorithmic qubits: 7
Gate breakdown:
{'Hadamard': 25, 'X': 5, 'CNOT': 6, 'Z': 5}
Similarly, users can scale up the resources for an operator by some integer factor using
the :code:`*` and :code:`@` operators. These represent scaling the resources assuming the
circuits were executed in series or parallel respectively.
.. code-block:: pycon
>>> res_in_series = 5 * res1
>>> print(res_in_series)
--- Resources: ---
Total qubits: 10
Total gates : 85
Qubit breakdown:
clean qubits: 2, dirty qubits: 5, algorithmic qubits: 3
Gate breakdown:
{'Hadamard': 50, 'X': 25, 'CNOT': 10}
>>> res_in_parallel = 5 @ res1
>>> print(res_in_parallel)
--- Resources: ---
Total qubits: 22
Total gates : 85
Qubit breakdown:
clean qubits: 2, dirty qubits: 5, algorithmic qubits: 15
Gate breakdown:
{'Hadamard': 50, 'X': 25, 'CNOT': 10}
"""
def __init__(self, qubit_manager: QubitManager, gate_types: dict = None):
"""Initialize the Resources class."""
gate_types = gate_types or {}
self.qubit_manager = qubit_manager
self.gate_types = (
gate_types
if (isinstance(gate_types, defaultdict) and isinstance(gate_types.default_factory, int))
else defaultdict(int, gate_types)
)
def __add__(self, other: "Resources") -> "Resources":
"""Add two resources objects in series"""
assert isinstance(other, self.__class__)
return add_in_series(self, other)
def __and__(self, other: "Resources") -> "Resources":
"""Add two resources objects in parallel"""
assert isinstance(other, self.__class__)
return add_in_parallel(self, other)
def __eq__(self, other: "Resources") -> bool:
"""Determine if two resources objects are equal"""
return (self.gate_types == other.gate_types) and (self.qubit_manager == other.qubit_manager)
def __mul__(self, scalar: int) -> "Resources":
"""Scale a resources object in series"""
assert isinstance(scalar, int)
return mul_in_series(self, scalar)
def __matmul__(self, scalar: int) -> "Resources":
"""Scale a resources object in parallel"""
assert isinstance(scalar, int)
return mul_in_parallel(self, scalar)
__rmul__ = __mul__
__radd__ = __add__
__rand__ = __and__
__rmatmul__ = __matmul__
@property
def clean_gate_counts(self):
r"""Produce a dictionary which stores the gate counts
using the operator names as keys.
Returns:
dict: A dictionary with operator names (str) as keys
and the number of occurances in the circuit (int) as values.
"""
clean_gate_counts = defaultdict(int)
for cmp_res_op, counts in self.gate_types.items():
clean_gate_counts[cmp_res_op.name] += counts
return clean_gate_counts
def __str__(self):
"""Generates a string representation of the Resources object."""
qm = self.qubit_manager
total_qubits = qm.total_qubits
total_gates = sum(self.clean_gate_counts.values())
total_gates_str = str(total_gates) if total_gates <= 999 else f"{Decimal(total_gates):.3E}"
total_qubits_str = (
str(total_qubits) if total_qubits <= 9999 else f"{Decimal(total_qubits):.3E}"
)
items = "--- Resources: ---\n"
items += f" Total qubits: {total_qubits_str}\n"
items += f" Total gates : {total_gates_str}\n"
qubit_breakdown_str = f"clean qubits: {qm.clean_qubits}, dirty qubits: {qm.dirty_qubits}, algorithmic qubits: {qm.algo_qubits}"
items += f" Qubit breakdown:\n {qubit_breakdown_str}\n"
gate_type_str = ", ".join(
[
f"'{gate_name}': {Decimal(count):.3E}" if count > 999 else f"'{gate_name}': {count}"
for gate_name, count in self.clean_gate_counts.items()
]
)
items += " Gate breakdown:\n {" + gate_type_str + "}"
return items
def __repr__(self):
"""Compact string representation of the Resources object"""
return {
"qubit_manager": self.qubit_manager,
"gate_types": self.gate_types,
}.__repr__()
def _ipython_display_(self): # pragma: no cover
"""Displays __str__ in ipython instead of __repr__"""
print(str(self))
def add_in_series(first: Resources, other) -> Resources: # +
r"""Add two resources assuming the circuits are executed in series.
Args:
first (Resources): first resource object to combine
other (Resources): other resource object to combine with
Returns:
Resources: combined resources
"""
qm1, qm2 = (first.qubit_manager, other.qubit_manager)
new_clean = max(qm1.clean_qubits, qm2.clean_qubits)
new_dirty = qm1.dirty_qubits + qm2.dirty_qubits
new_budget = qm1.tight_budget or qm2.tight_budget
new_logic = max(qm1.algo_qubits, qm2.algo_qubits)
new_qubit_manager = QubitManager(
work_wires={"clean": new_clean, "dirty": new_dirty}, tight_budget=new_budget
)
new_qubit_manager.algo_qubits = new_logic
new_gate_types = _combine_dict(first.gate_types, other.gate_types)
return Resources(new_qubit_manager, new_gate_types)
def add_in_parallel(first: Resources, other) -> Resources: # &
r"""Add two resources assuming the circuits are executed in parallel.
Args:
first (Resources): first resource object to combine
other (Resources): other resource object to combine with
Returns:
Resources: combined resources
"""
qm1, qm2 = (first.qubit_manager, other.qubit_manager)
new_clean = max(qm1.clean_qubits, qm2.clean_qubits)
new_dirty = qm1.dirty_qubits + qm2.dirty_qubits
new_budget = qm1.tight_budget or qm2.tight_budget
new_logic = qm1.algo_qubits + qm2.algo_qubits
new_qubit_manager = QubitManager(
work_wires={"clean": new_clean, "dirty": new_dirty},
tight_budget=new_budget,
)
new_qubit_manager.algo_qubits = new_logic
new_gate_types = _combine_dict(first.gate_types, other.gate_types)
return Resources(new_qubit_manager, new_gate_types)
def mul_in_series(first: Resources, scalar: int) -> Resources: # *
r"""Multiply the resources by a scalar assuming the circuits are executed in series.
Args:
first (Resources): first resource object to scale
scalar (int): integer value to scale the resources by
Returns:
Resources: scaled resources
"""
qm = first.qubit_manager
new_clean = qm.clean_qubits
new_dirty = scalar * qm.dirty_qubits
new_budget = qm.tight_budget
new_logic = qm.algo_qubits
new_qubit_manager = QubitManager(
work_wires={"clean": new_clean, "dirty": new_dirty},
tight_budget=new_budget,
)
new_qubit_manager.algo_qubits = new_logic
new_gate_types = _scale_dict(first.gate_types, scalar)
return Resources(new_qubit_manager, new_gate_types)
def mul_in_parallel(first: Resources, scalar: int) -> Resources: # @
r"""Multiply the resources by a scalar assuming the circuits are executed in parallel.
Args:
first (Resources): first resource object to scale
scalar (int): integer value to scale the resources by
Returns:
Resources: scaled resources
"""
qm = first.qubit_manager
new_clean = qm.clean_qubits
new_dirty = scalar * qm.dirty_qubits
new_budget = qm.tight_budget
new_logic = scalar * qm.algo_qubits
new_qubit_manager = QubitManager(
work_wires={"clean": new_clean, "dirty": new_dirty},
tight_budget=new_budget,
)
new_qubit_manager.algo_qubits = new_logic
new_gate_types = _scale_dict(first.gate_types, scalar)
return Resources(new_qubit_manager, new_gate_types)
def _combine_dict(dict1: defaultdict, dict2: defaultdict) -> defaultdict:
r"""Private function which combines two dictionaries together."""
combined_dict = copy.copy(dict1)
for k, v in dict2.items():
combined_dict[k] += v
return combined_dict
def _scale_dict(dict1: defaultdict, scalar: int) -> defaultdict:
r"""Private function which scales the values in a dictionary."""
scaled_dict = copy.copy(dict1)
for k in scaled_dict:
scaled_dict[k] *= scalar
return scaled_dict
_modules/pennylane/labs/resource_estimation/resources_base
Download Python script
Download Notebook
View on GitHub