Source code for pennylane.labs.resource_estimation.resource_container
# Copyright 2024 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 classes for resource estimation."""
import copy
from collections import defaultdict
from pennylane.labs.resource_estimation import ResourceOperator
[docs]class CompressedResourceOp:
r"""Instantiate the light weight class corresponding to the operator type and parameters.
Args:
op_type (Type): the class object of an operation which inherits from '~.ResourceOperator'
params (dict): a dictionary containing the minimal pairs of parameter names and values
required to compute the resources for the given operator
.. details::
This representation is the minimal amount of information required to estimate resources for the operator.
**Example**
>>> op_tp = CompressedResourceOp(ResourceHadamard, {"num_wires":1})
>>> print(op_tp)
Hadamard(num_wires=1)
"""
def __init__(self, op_type, params: dict, name=None) -> None:
r"""Instantiate the light weight class corresponding to the operator type and parameters.
Args:
op_type (Type): the class object for an operation which inherits from '~.ResourceOperator'
params (dict): a dictionary containing the minimal pairs of parameter names and values
required to compute the resources for the given operator
.. details::
This representation is the minimal amount of information required to estimate resources for the operator.
**Example**
>>> op_tp = CompressedResourceOp(ResourceHadamard, {"num_wires":1})
>>> print(op_tp)
Hadamard(num_wires=1)
"""
if not issubclass(op_type, ResourceOperator):
raise TypeError(f"op_type must be a subclass of ResourceOperator. Got {op_type}.")
self.op_type = op_type
self.params = params
self._hashable_params = _make_hashable(params)
self._name = name or op_type.tracking_name(**params)
def __hash__(self) -> int:
return hash((self._name, self._hashable_params))
def __eq__(self, other: object) -> bool:
return (self.op_type == other.op_type) and (self.params == other.params)
def __repr__(self) -> str:
return self._name
# @dataclass
[docs]class Resources:
r"""Contains attributes which store key resources such as number of gates, number of wires, and gate types.
Args:
num_wires (int): number of qubits
num_gates (int): number of gates
gate_types (dict): dictionary storing operation names (str) as keys
and the number of times they are used in the circuit (int) as values
.. details::
The resources being tracked can be accessed as class attributes.
Additionally, the :code:`Resources` instance can be nicely displayed in the console.
**Example**
>>> r = Resources(
... num_wires=2,
... num_gates=2,
... gate_types={"Hadamard": 1, "CNOT": 1}
... )
>>> print(r)
wires: 2
gates: 2
gate_types:
{'Hadamard': 1, 'CNOT': 1}
"""
def __init__(self, num_wires: int = 0, num_gates: int = 0, gate_types: dict = None):
gate_types = gate_types or {}
self.num_wires = num_wires
self.num_gates = num_gates
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"""
return add_in_series(self, other)
def __eq__(self, other: "Resources") -> bool:
"""Test if two resource objects are equal"""
if self.num_wires != other.num_wires:
return False
if self.num_gates != other.num_gates:
return False
return self.gate_types == other.gate_types
def __mul__(self, scalar: int) -> "Resources":
"""Scale a resources object in series"""
return mul_in_series(self, scalar)
__rmul__ = __mul__ # same implementation
def __iadd__(self, other: "Resources") -> "Resources":
"""Add two resources objects in series"""
return add_in_series(self, other, in_place=True)
def __imull__(self, scalar: int) -> "Resources":
"""Scale a resources object in series"""
return mul_in_series(self, scalar, in_place=True)
def __str__(self):
"""String representation of the Resources object."""
keys = ["wires", "gates"]
vals = [self.num_wires, self.num_gates]
items = "\n".join([str(i) for i in zip(keys, vals)])
items = items.replace("('", "")
items = items.replace("',", ":")
items = items.replace(")", "")
gate_type_str = ", ".join(
[f"'{gate_name}': {count}" for gate_name, count in self.gate_types.items()]
)
items += "\ngate_types:\n{" + gate_type_str + "}"
return items
def __repr__(self):
"""Compact string representation of the Resources object"""
return {
"gate_types": self.gate_types,
"num_gates": self.num_gates,
"num_wires": self.num_wires,
}.__repr__()
def _ipython_display_(self):
"""Displays __str__ in ipython instead of __repr__"""
print(str(self))
[docs]def add_in_series(first: Resources, other: Resources, in_place=False) -> 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
in_place (bool): determines if the first Resources are modified in place (default False)
Returns:
Resources: combined resources
"""
new_wires = max(first.num_wires, other.num_wires)
new_gates = first.num_gates + other.num_gates
new_gate_types = _combine_dict(first.gate_types, other.gate_types, in_place=in_place)
if in_place:
first.num_wires = new_wires
first.num_gates = new_gates
return first
return Resources(new_wires, new_gates, new_gate_types)
[docs]def add_in_parallel(first: Resources, other: Resources, in_place=False) -> 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
in_place (bool): determines if the first Resources are modified in place (default False)
Returns:
Resources: combined resources
"""
new_wires = first.num_wires + other.num_wires
new_gates = first.num_gates + other.num_gates
new_gate_types = _combine_dict(first.gate_types, other.gate_types, in_place=in_place)
if in_place:
first.num_wires = new_wires
first.num_gates = new_gates
return first
return Resources(new_wires, new_gates, new_gate_types)
[docs]def mul_in_series(first: Resources, scalar: int, in_place=False) -> Resources:
r"""Multiply the resources by a scalar assuming the circuits are executed in series.
Args:
first (Resources): first resource object to combine
scalar (int): integer value to scale the resources by
in_place (bool): determines if the first Resources are modified in place (default False)
Returns:
Resources: combined resources
"""
new_gates = scalar * first.num_gates
new_gate_types = _scale_dict(first.gate_types, scalar, in_place=in_place)
if in_place:
first.num_gates = new_gates
return first
return Resources(first.num_wires, new_gates, new_gate_types)
[docs]def mul_in_parallel(first: Resources, scalar: int, in_place=False) -> Resources:
r"""Multiply the resources by a scalar assuming the circuits are executed in parallel.
Args:
first (Resources): first resource object to combine
scalar (int): integer value to scale the resources by
in_place (bool): determines if the first Resources are modified in place (default False)
Returns:
Resources: combined resources
"""
new_wires = scalar * first.num_wires
new_gates = scalar * first.num_gates
new_gate_types = _scale_dict(first.gate_types, scalar, in_place=in_place)
if in_place:
first.num_wires = new_wires
first.num_gates = new_gates
return first
return Resources(new_wires, new_gates, new_gate_types)
[docs]def substitute(
initial_resources: Resources, gate_name: str, replacement_resources: Resources, in_place=False
) -> Resources:
"""Replaces a specified gate in a :class:`~.resource.Resources` object with the contents of another :class:`~.resource.Resources` object.
Args:
initial_resources (Resources): the resources to be modified
gate_name (str): the name of the operation to be replaced
replacement (Resources): the resources to be substituted instead of the gate
in_place (bool): determines if the initial resources are modified in place or if a new copy is created
Returns:
Resources: the updated :class:`~.Resources` after substitution
.. details::
**Example**
In this example we replace the resources for the :code:`RX` gate. First we build the :class:`~.Resources`:
.. code-block:: python3
from pennylane.labs.resource_estimation import Resources
replace_gate_name = "RX"
initial_resources = Resources(
num_wires = 2,
num_gates = 3,
gate_types = {"RX": 2, "CNOT": 1},
)
replacement_rx_resources = Resources(
num_wires = 1,
num_gates = 7,
gate_types = {"Hadamard": 3, "S": 4},
)
Executing the substitution produces:
>>> from pennylane.labs.resource_estimation import substitute
>>> res = substitute(
... initial_resources, replace_gate_name, replacement_rx_resources,
... )
>>> print(res)
wires: 2
gates: 15
gate_types:
{'CNOT': 1, 'Hadamard': 6, 'S': 8}
"""
count = initial_resources.gate_types.get(gate_name, 0)
if count > 0:
new_gates = initial_resources.num_gates - count + (count * replacement_resources.num_gates)
replacement_gate_types = _scale_dict(
replacement_resources.gate_types, count, in_place=in_place
)
new_gate_types = _combine_dict(
initial_resources.gate_types, replacement_gate_types, in_place=in_place
)
new_gate_types.pop(gate_name)
if in_place:
initial_resources.num_gates = new_gates
return initial_resources
return Resources(initial_resources.num_wires, new_gates, new_gate_types)
return initial_resources
def _combine_dict(dict1: defaultdict, dict2: defaultdict, in_place=False):
r"""Private function which combines two dictionaries together."""
combined_dict = dict1 if in_place else copy.copy(dict1)
for k, v in dict2.items():
combined_dict[k] += v
return combined_dict
def _scale_dict(dict1: defaultdict, scalar: int, in_place=False):
r"""Private function which scales the values in a dictionary."""
combined_dict = dict1 if in_place else copy.copy(dict1)
for k in combined_dict:
combined_dict[k] *= scalar
return combined_dict
def _make_hashable(d) -> tuple:
if isinstance(d, dict):
return tuple((name, _make_hashable(value)) for name, value in d.items())
return d
_modules/pennylane/labs/resource_estimation/resource_container
Download Python script
Download Notebook
View on GitHub