Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add and Multiply operations for Resource objects #6567

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
* Shortened the string representation for the `qml.S`, `qml.T`, and `qml.SX` operators.
[(#6542)](https://github.com/PennyLaneAI/pennylane/pull/6542)

* Added functions, dunder methods, to add and multiply Resources objects in series and in parallel
[(#6567)](https://github.com/PennyLaneAI/pennylane/pull/6567)

<h4>Capturing and representing hybrid programs</h4>

* `jax.vmap` can be captured with `qml.capture.make_plxpr` and is compatible with quantum circuits.
Expand Down
22 changes: 21 additions & 1 deletion pennylane/resource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@
~Resources
~ResourcesOperation

Resource Functions
~~~~~~~~~~~~~~~~~~

.. currentmodule:: pennylane.resource

.. autosummary::
:toctree: api

~add_in_series
~add_in_parallel
~mul_in_series
~mul_in_parallel

Tracking Resources for Custom Operations
----------------------------------------

Expand Down Expand Up @@ -122,6 +135,13 @@ def circuit(theta):
from .error import AlgorithmicError, ErrorOperation, SpectralNormError
from .first_quantization import FirstQuantization
from .measurement import estimate_error, estimate_shots
from .resource import Resources, ResourcesOperation
from .resource import (
Resources,
ResourcesOperation,
add_in_series,
add_in_parallel,
mul_in_series,
mul_in_parallel,
)
from .second_quantization import DoubleFactorization
from .specs import specs
243 changes: 243 additions & 0 deletions pennylane/resource/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
"""
Stores classes and logic to aggregate all the resource information from a quantum workflow.
"""
from __future__ import annotations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from __future__ import annotations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed for the type hints to work. Is there a problem with this import?


import copy
from abc import abstractmethod
from collections import defaultdict
from collections.abc import Sequence

Check notice on line 22 in pennylane/resource/resource.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane/resource/resource.py#L22

Unused Sequence imported from collections.abc (unused-import)
from dataclasses import dataclass, field

from pennylane.measurements import Shots
Expand Down Expand Up @@ -63,6 +67,60 @@
depth: int = 0
shots: Shots = field(default_factory=Shots)

def __add__(self, other: Resources):
r"""Adds two 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**

>>> r1 = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> r2 = Resources(num_wires=2, num_gates=2, gate_types={'RX': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> print(r1 + r2)
wires: 2
gates: 4
depth: 0
shots: Shots(total=None)
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 Resource 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**
>>> r1 = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> print(r1 * 2)
wires: 2
gates: 4
depth: 0
shots: Shots(total=None)
gate_types:
{'Hadamard': 2, 'CNOT': 2}
gate_sizes:
{1: 2, 2: 2}
"""
return mul_in_series(self, scalar)

__rmul__ = __mul__

def __str__(self):
keys = ["wires", "gates", "depth"]
vals = [self.num_wires, self.num_gates, self.depth]
Expand Down Expand Up @@ -125,6 +183,177 @@
"""


def add_in_series(r1: Resources, r2: Resources) -> Resources:
r"""Add two resources assuming the circuits are executed in series.

Args:
r1 (Resources): A Resources object to add.
r2 (Resources): A Resources object to add.

Returns:
Resources: The combined resources of r1 and r2.

.. details::

**Example**

>>> r1 = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> r2 = Resources(num_wires=2, num_gates=2, gate_types={'RX': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> print(r1 + r2)
wires: 2
gates: 4
depth: 0
shots: Shots(total=None)
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)


def add_in_parallel(r1: Resources, r2: Resources) -> Resources:
r"""Add two resources assuming the circuits are executed in parallel.

Args:
r1 (Resources): A Resources object to add.
r2 (Resources): A Resources object to add.

Returns:
Resources: The combined resources of r1 and r2.

.. details::

**Example**

>>> r1 = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> r2 = Resources(num_wires=2, num_gates=2, gate_types={'RX': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> print(add_in_parallel(r1, r2))
wires: 4
gates: 4
depth: 0
shots: Shots(total=None)
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)


def mul_in_series(r1: Resources, scalar: int) -> Resources:
"""Multiply the Resources object by a scalar as if the circuit was repeated
that many times in series.

Args:
r1 (Resources): A Resources object to be scaled.
scalar (int): The scalar to multiply the Resources object by.

Returns:
Resources: The combined resources

.. details::

**Example**

>>> r1 = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> print(r1 * 2)
wires: 2
gates: 4
depth: 0
shots: Shots(total=None)
gate_types:
{'Hadamard': 2, 'CNOT': 2}
gate_sizes:
{1: 2, 2: 2}
"""

new_wires = r1.num_wires
new_gates = scalar * r1.num_gates
new_gate_types = _scale_dict(r1.gate_types, scalar)
new_gate_sizes = _scale_dict(r1.gate_sizes, scalar)
new_shots = scalar * r1.shots
new_depth = scalar * r1.depth

return Resources(new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, new_shots)


def mul_in_parallel(r1: Resources, scalar: int) -> Resources:
"""Multiply the Resources object by a scalar as if the circuit was repeated
that many times in parallel.

Args:
r1 (Resources): A Resources object to be scaled.
scalar (int): The scalar to multiply the Resources object by.

Returns:
Resources: The combined resources

.. details::

**Example**

>>> r1 = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1})
>>> print(mul_in_parallel(r1, 2))
wires: 4
gates: 4
depth: 0
shots: Shots(total=None)
gate_types:
{'Hadamard': 2, 'CNOT': 2}
gate_sizes:
{1: 2, 2: 2}
"""

new_wires = scalar * r1.num_wires
new_gates = scalar * r1.num_gates
new_gate_types = _scale_dict(r1.gate_types, scalar)
new_gate_sizes = _scale_dict(r1.gate_sizes, scalar)
new_shots = scalar * r1.shots

return Resources(new_wires, new_gates, new_gate_types, new_gate_sizes, r1.depth, new_shots)


def _combine_dict(dict1: dict, dict2: dict):
r"""Private function which combines two dictionaries together."""
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"""Private function which scales the values in a dictionary."""

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.
Expand Down Expand Up @@ -159,3 +388,17 @@
num_gates += 1

return Resources(num_wires, num_gates, gate_types, gate_sizes, depth, shots)


def _add_shots(s1: Shots, s2: Shots) -> Shots:
if s1.total_shots is None:
return s2

if s2.total_shots is None:
return s1

shot_vector = []
for shot in s1.shot_vector + s2.shot_vector:
shot_vector.append((shot.shots, shot.copies))

return Shots(shots=shot_vector)
Comment on lines +392 to +403
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, I just wonder if it's ideal to have this defined here, or better in pennylane/measurements/shots.py as part of the Shots class.
Probably best to check with Soran.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking the same thing, but I didn't want to add an unrelated file to the PR. I'll ask Soran.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current location looks good to me. @Jaybsoni please also check.

Loading
Loading