Source code for pennylane.resource.resource
# Copyright 2018-2023 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.
"""
Stores classes and logic to aggregate all the resource information from a quantum workflow.
"""
from __future__ import annotations
import copy
from abc import abstractmethod
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Tuple
from pennylane.measurements import Shots, add_shots
from pennylane.operation import Operation
[docs]@dataclass(frozen=True)
class Resources:
r"""Contains attributes which store key resources such as number of gates, number of wires, shots,
depth 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
gate_sizes (dict): dictionary storing the number of :math:`n` qubit gates in the circuit
as a key-value pair where :math:`n` is the key and the number of occurances is the value
depth (int): the depth of the circuit defined as the maximum number of non-parallel operations
shots (Shots): number of samples to generate
.. details::
The resources being tracked can be accessed as class attributes.
Additionally, the :code:`Resources` instance can be nicely displayed in the console.
**Example**
>>> from pennylane.resource import Resources
>>> r = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1}, depth=2)
>>> print(r)
num_wires: 2
num_gates: 2
depth: 2
shots: Shots(total=None)
gate_types:
{'Hadamard': 1, 'CNOT': 1}
gate_sizes:
{1: 1, 2: 1}
:class:`~.Resources` objects can be added together or multiplied by a scalar.
>>> from pennylane.resource import Resources
>>> r1 = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1}, depth=2)
>>> r2 = Resources(num_wires=2, num_gates=2, gate_types={'RX': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1}, depth=2)
>>> print(r1 + r2)
wires: 2
gates: 4
depth: 4
shots: Shots(total=None)
gate_types:
{'Hadamard': 1, 'CNOT': 2, 'RX': 1}
gate_sizes:
{1: 2, 2: 2}
>>> print(r1 * 2)
wires: 2
gates: 4
depth: 4
shots: Shots(total=None)
gate_types:
{'Hadamard': 2, 'CNOT': 2}
gate_sizes:
{1: 2, 2: 2}
"""
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
shots: Shots = field(default_factory=Shots)
def __add__(self, other: Resources):
r"""Adds two :class:`~resource.Resources` objects together as if the circuits were executed in series.
Args:
other (Resources): the resource object to add
Returns:
Resources: the combined resources
.. details::
**Example**
First we build two :class:`~.resource.Resources` objects.
.. code-block:: python3
from pennylane.measurements import Shots
from pennylane.resource import Resources
r1 = Resources(
num_wires = 2,
num_gates = 2,
gate_types = {"Hadamard": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 2,
shots = Shots(10)
)
r2 = Resources(
num_wires = 3,
num_gates = 2,
gate_types = {"RX": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 1,
shots = Shots((5, (2, 10)))
)
Now we print their sum.
>>> print(r1 + r2)
wires: 3
gates: 4
depth: 3
shots: Shots(total=35, vector=[10 shots, 5 shots, 2 shots x 10])
gate_types:
{'Hadamard': 1, 'CNOT': 2, 'RX': 1}
gate_sizes:
{1: 2, 2: 2}
"""
return add_in_series(self, other)
def __mul__(self, scalar: int):
r"""Multiply the :class:`~resource.Resources` object by a scalar as if that many copies of the circuit were executed in series
Args:
scalar (int): the scalar to multiply the resource object by
Returns:
Resources: the combined resources
.. details::
**Example**
First we build a :class:`~.resource.Resources` object.
.. code-block:: python3
from pennylane.measurements import Shots
from pennylane.resource import Resources
resources = Resources(
num_wires = 2,
num_gates = 2,
gate_types = {"Hadamard": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 2,
shots = Shots(10)
)
Now we print the product.
>>> print(resources * 2)
wires: 2
gates: 4
depth: 4
shots: Shots(total=20)
gate_types:
{'Hadamard': 2, 'CNOT': 2}
gate_sizes:
{1: 2, 2: 2}
"""
return mul_in_series(self, scalar)
__rmul__ = __mul__
def __str__(self):
keys = ["num_wires", "num_gates", "depth"]
vals = [self.num_wires, self.num_gates, self.depth]
items = "\n".join([str(i) for i in zip(keys, vals)])
items = items.replace("('", "")
items = items.replace("',", ":")
items = items.replace(")", "")
items += f"\nshots: {str(self.shots)}"
gate_type_str = ", ".join(
[f"'{gate_name}': {count}" for gate_name, count in self.gate_types.items()]
)
items += "\ngate_types:\n{" + gate_type_str + "}"
gate_size_str = ", ".join(
[f"{n_gate}: {count}" for n_gate, count in self.gate_sizes.items()]
)
items += "\ngate_sizes:\n{" + gate_size_str + "}"
return items
def _ipython_display_(self):
"""Displays __str__ in ipython instead of __repr__"""
print(str(self))
[docs]class ResourcesOperation(Operation):
r"""Base class that represents quantum gates or channels applied to quantum
states and stores the resource requirements of the quantum gate.
.. note::
Child classes must implement the :func:`~.ResourcesOperation.resources` method which computes
the resource requirements of the operation.
"""
[docs] @abstractmethod
def resources(self) -> Resources:
r"""Compute the resources required for this operation.
Returns:
Resources: The resources required by this operation.
**Examples**
>>> class CustomOp(ResourcesOperation):
... num_wires = 2
... def resources(self):
... return Resources(num_wires=self.num_wires, num_gates=3, depth=2)
...
>>> op = CustomOp(wires=[0, 1])
>>> print(op.resources())
num_wires: 2
num_gates: 3
depth: 2
shots: Shots(total=None)
gate_types:
{}
gate_sizes:
{}
"""
[docs]def add_in_series(r1: Resources, r2: Resources) -> Resources:
r"""
Add two :class:`~.resource.Resources` objects assuming the circuits are executed in series.
The gates in ``r1`` and ``r2`` are assumed to act on the same qubits. The resulting circuit
depth is the sum of the depths of ``r1`` and ``r2``. To add resources as if they were executed
in parallel see :func:`~.resource.add_in_parallel`.
Args:
r1 (Resources): a :class:`~resource.Resources` to add
r2 (Resources): a :class:`~resource.Resources` to add
Returns:
Resources: the combined resources
.. details::
**Example**
First we build two :class:`~.resource.Resources` objects.
.. code-block:: python3
from pennylane.measurements import Shots
from pennylane.resource import Resources
r1 = Resources(
num_wires = 2,
num_gates = 2,
gate_types = {"Hadamard": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 2,
shots = Shots(10)
)
r2 = Resources(
num_wires = 3,
num_gates = 2,
gate_types = {"RX": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 1,
shots = Shots((5, (2, 10)))
)
Now we print their sum.
>>> print(qml.resource.add_in_series(r1, r2))
wires: 3
gates: 4
depth: 3
shots: Shots(total=35, vector=[10 shots, 5 shots, 2 shots x 10])
gate_types:
{'Hadamard': 1, 'CNOT': 2, 'RX': 1}
gate_sizes:
{1: 2, 2: 2}
"""
new_wires = max(r1.num_wires, r2.num_wires)
new_gates = r1.num_gates + r2.num_gates
new_gate_types = _combine_dict(r1.gate_types, r2.gate_types)
new_gate_sizes = _combine_dict(r1.gate_sizes, r2.gate_sizes)
new_shots = add_shots(r1.shots, r2.shots)
new_depth = r1.depth + r2.depth
return Resources(new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, new_shots)
[docs]def add_in_parallel(r1: Resources, r2: Resources) -> Resources:
r"""
Add two :class:`~.resource.Resources` objects assuming the circuits are executed in parallel.
The gates in ``r2`` and ``r2`` are assumed to act on disjoint sets of qubits. The resulting
circuit depth is the max depth of ``r1`` and ``r2``. To add resources as if they were executed
in series see :func:`~.resource.add_in_series`.
Args:
r1 (Resources): a :class:`~.resource.Resources` object to add
r2 (Resources): a :class:`~.resource.Resources` object to add
Returns:
Resources: the combined resources
.. details::
**Example**
First we build two :class:`~.resource.Resources` objects.
.. code-block:: python3
from pennylane.measurements import Shots
from pennylane.resource import Resources
r1 = Resources(
num_wires = 2,
num_gates = 2,
gate_types = {"Hadamard": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 2,
shots = Shots(10)
)
r2 = Resources(
num_wires = 3,
num_gates = 2,
gate_types = {"RX": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 1,
shots = Shots((5, (2, 10)))
)
Now we print their sum.
>>> print(qml.resource.add_in_parallel(r1, r2))
wires: 5
gates: 4
depth: 2
shots: Shots(total=35, vector=[10 shots, 5 shots, 2 shots x 10])
gate_types:
{'Hadamard': 1, 'CNOT': 2, 'RX': 1}
gate_sizes:
{1: 2, 2: 2}
"""
new_wires = r1.num_wires + r2.num_wires
new_gates = r1.num_gates + r2.num_gates
new_gate_types = _combine_dict(r1.gate_types, r2.gate_types)
new_gate_sizes = _combine_dict(r1.gate_sizes, r2.gate_sizes)
new_shots = add_shots(r1.shots, r2.shots)
new_depth = max(r1.depth, r2.depth)
return Resources(new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, new_shots)
[docs]def mul_in_series(resources: Resources, scalar: int) -> Resources:
"""
Multiply the :class:`~resource.Resources` object by a scalar as if the circuit was repeated that many times in series.
The repeated copies of ``resources`` are assumed to act on the same
wires as ``resources``. The resulting circuit depth is the depth of ``resources`` multiplied by
``scalar``. To multiply as if the circuit was repeated in parallel see
:func:`~.resource.mul_in_parallel`.
Args:
resources (Resources): a :class:`~resource.Resources` to be scaled
scalar (int): the scalar to multiply the :class:`~resource.Resources` by
Returns:
Resources: the combined resources
.. details::
**Example**
First we build a :class:`~.resource.Resources` object.
.. code-block:: python3
from pennylane.measurements import Shots
from pennylane.resource import Resources
resources = Resources(
num_wires = 2,
num_gates = 2,
gate_types = {"Hadamard": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 2,
shots = Shots(10)
)
Now we print the product.
>>> print(qml.resource.mul_in_series(resources, 2))
wires: 2
gates: 4
depth: 4
shots: Shots(total=20)
gate_types:
{'Hadamard': 2, 'CNOT': 2}
gate_sizes:
{1: 2, 2: 2}
"""
new_wires = resources.num_wires
new_gates = scalar * resources.num_gates
new_gate_types = _scale_dict(resources.gate_types, scalar)
new_gate_sizes = _scale_dict(resources.gate_sizes, scalar)
new_shots = scalar * resources.shots
new_depth = scalar * resources.depth
return Resources(new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, new_shots)
[docs]def mul_in_parallel(resources: Resources, scalar: int) -> Resources:
"""
Multiply the :class:`~resource.Resources` object by a scalar as if the circuit was repeated that many times in parallel.
The repeated copies of ``resources`` are assumed to act on disjoint qubits. The resulting circuit
depth is equal to the depth of ``resources``. To multiply as if the repeated copies were
executed in series see :func:`~.resource.mul_in_series`.
Args:
resources (Resources): a :class:`~resource.Resources` to be scaled
scalar (int): the scalar to multiply the :class:`~resource.Resources` by
Returns:
Resources: The combined resources
.. details::
**Example**
First we build a :class:`~.resource.Resources` object.
.. code-block:: python3
from pennylane.measurements import Shots
from pennylane.resource import Resources
resources = Resources(
num_wires = 2,
num_gates = 2,
gate_types = {"Hadamard": 1, "CNOT": 1},
gate_sizes = {1: 1, 2: 1},
depth = 2,
shots = Shots(10)
)
Now we print the product.
>>> print(qml.resource.mul_in_parallel(resources, 2))
wires: 4
gates: 4
depth: 2
shots: Shots(total=20)
gate_types:
{'Hadamard': 2, 'CNOT': 2}
gate_sizes:
{1: 2, 2: 2}
"""
new_wires = scalar * resources.num_wires
new_gates = scalar * resources.num_gates
new_gate_types = _scale_dict(resources.gate_types, scalar)
new_gate_sizes = _scale_dict(resources.gate_sizes, scalar)
new_shots = scalar * resources.shots
return Resources(
new_wires, new_gates, new_gate_types, new_gate_sizes, resources.depth, new_shots
)
[docs]def substitute(initial_resources: Resources, gate_info: Tuple[str, int], replacement: 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 :class:`~resource.Resources` object to be modified
gate_info (Iterable(str, int)): sequence containing the name of the gate to be replaced and the number of wires it acts on
replacement (Resources): the :class:`~resource.Resources` containing the resources that will replace the gate
Returns:
Resources: the updated :class:`~resource.Resources` after substitution
.. details::
**Example**
First we build the :class:`~.resource.Resources`.
.. code-block:: python3
from pennylane.measurements import Shots
from pennylane.resource import Resources
initial_resources = Resources(
num_wires = 2,
num_gates = 3,
gate_types = {"RX": 2, "CNOT": 1},
gate_sizes = {1: 2, 2: 1},
depth = 2,
shots = Shots(10)
)
# the RX gates will be replaced by the substitution
gate_info = ("RX", 1)
replacement = Resources(
num_wires = 1,
num_gates = 7,
gate_types = {"Hadamard": 3, "S": 4},
gate_sizes = {1: 7},
depth = 7
)
Now we print the result of the substitution.
>>> res = qml.resource.substitute(initial_resources, gate_info, replacement)
>>> print(res)
wires: 2
gates: 15
depth: 9
shots: Shots(total=10)
gate_types:
{'CNOT': 1, 'H': 6, 'S': 8}
gate_sizes:
{1: 14, 2: 1}
"""
gate_name, num_wires = gate_info
if not num_wires in initial_resources.gate_sizes:
raise ValueError(f"initial_resources does not contain a gate acting on {num_wires} wires.")
gate_count = initial_resources.gate_types.get(gate_name, 0)
if gate_count > initial_resources.gate_sizes[num_wires]:
raise ValueError(
f"Found {gate_count} gates of type {gate_name}, but only {initial_resources.gate_sizes[num_wires]} gates act on {num_wires} wires in initial_resources."
)
if gate_count > 0:
new_wires = initial_resources.num_wires
new_gates = initial_resources.num_gates - gate_count + (gate_count * replacement.num_gates)
replacement_gate_types = _scale_dict(replacement.gate_types, gate_count)
replacement_gate_sizes = _scale_dict(replacement.gate_sizes, gate_count)
new_gate_types = _combine_dict(initial_resources.gate_types, replacement_gate_types)
new_gate_types.pop(gate_name)
new_gate_sizes = copy.copy(initial_resources.gate_sizes)
new_gate_sizes[num_wires] -= gate_count
new_gate_sizes = _combine_dict(new_gate_sizes, replacement_gate_sizes)
new_depth = initial_resources.depth + replacement.depth
wire_diff = num_wires - replacement.num_wires
if wire_diff < 0:
new_wires = initial_resources.num_wires + abs(wire_diff)
else:
new_wires = initial_resources.num_wires
return Resources(
new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, initial_resources.shots
)
return initial_resources
def _combine_dict(dict1: dict, dict2: dict):
r"""Combines two dictionaries and adds values of common keys."""
combined_dict = copy.copy(dict1)
for k, v in dict2.items():
try:
combined_dict[k] += v
except KeyError:
combined_dict[k] = v
return combined_dict
def _scale_dict(dict1: dict, scalar: int):
r"""Scales the values in a dictionary with a scalar."""
combined_dict = copy.copy(dict1)
for k in combined_dict:
combined_dict[k] *= scalar
return combined_dict
def _count_resources(tape) -> Resources:
"""Given a quantum circuit (tape), this function
counts the resources used by standard PennyLane operations.
Args:
tape (.QuantumTape): The quantum circuit for which we count resources
Returns:
(.Resources): The total resources used in the workflow
"""
num_wires = len(tape.wires)
shots = tape.shots
depth = tape.graph.get_depth()
num_gates = 0
gate_types = defaultdict(int)
gate_sizes = defaultdict(int)
for op in tape.operations:
if isinstance(op, ResourcesOperation):
op_resource = op.resources()
for d in op_resource.gate_types:
gate_types[d] += op_resource.gate_types[d]
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_sizes[len(op.wires)] += 1
num_gates += 1
return Resources(num_wires, num_gates, gate_types, gate_sizes, depth, shots)
_modules/pennylane/resource/resource
Download Python script
Download Notebook
View on GitHub