Skip to content

Commit

Permalink
qml.specs now uses Resources class (#4015)
Browse files Browse the repository at this point in the history
* init commit of feature branch

* initial commit to add base resource class

* tests + docs

* lint

* lint + fix docs

* more lint + docs

* update implementation to prevent updating attributes

* lint

* Apply suggestions from code review

Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com>

* Address review comments, add test for __eq__ op

* lint

* Added tests

* pylint

* code review comment

* fix if

* fix typo

* Update pennylane/resource/resource.py

Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>

* code review comments

* swapped over to data class implementation

* keep default dict in count resource method

* fix type hint

* changelog

* lint

* ci

* make count_resources private

* Update pennylane/resource/resource.py

Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com>

* fixed docstring

* changelog

* remove support for custom ops

* Added spec support and fixed bug printing default dicts

* fix tests

* code factor

* fix tests

* fix docs

* Update pennylane/tape/qscript.py

Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com>

* add back missing dict keys, revert tests

* Update pennylane/tape/qscript.py

Co-authored-by: Utkarsh <utkarshazad98@gmail.com>

---------

Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com>
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
Co-authored-by: Utkarsh <utkarshazad98@gmail.com>
  • Loading branch information
4 people authored Apr 21, 2023
1 parent 7462ed9 commit a87fca3
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 120 deletions.
8 changes: 6 additions & 2 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
<class 'pennylane.operation.Tensor'>
```

* New `Resources` data class to store resources like number of gates and circuit depth throughout a
* The `Resources` data class is added to store resources like number of gates and circuit depth throughout a
quantum circuit.
[(#3981)](https://github.com/PennyLaneAI/pennylane/pull/3981/)

* A `_count_resources()` function was added to count the resources required when executing a
* The `_count_resources()` function is added to count the resources required when executing a
QuantumTape for a given number of shots.
[(#3996)](https://github.com/PennyLaneAI/pennylane/pull/3996)

Expand Down Expand Up @@ -253,6 +253,10 @@
* Update various Operators and templates to ensure their decompositions only return lists of Operators.
[(#3243)](https://github.com/PennyLaneAI/pennylane/pull/3243)

* `QuantumScript.specs` is modified to make use of the new `Resources` class. This also modifies the
output of `qml.specs()`.
[(#4015)](https://github.com/PennyLaneAI/pennylane/pull/4015)

<h3>Breaking changes 💔</h3>

* The `seed_recipes` argument has been removed from `qml.classical_shadow` and `qml.shadow_expval`.
Expand Down
14 changes: 8 additions & 6 deletions pennylane/resource/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
from collections import defaultdict
from dataclasses import dataclass, field

from pennylane.tape import QuantumTape


@dataclass(frozen=True)
class Resources:
Expand Down Expand Up @@ -55,21 +53,25 @@ class Resources:
shots: int = 0

def __str__(self):
keys = ["wires", "gates", "depth", "shots", "gate_types"]
vals = [self.num_wires, self.num_gates, self.depth, self.shots, self.gate_types]
keys = ["wires", "gates", "depth", "shots"]
vals = [self.num_wires, self.num_gates, self.depth, self.shots]
items = "\n".join([str(i) for i in zip(keys, vals)])
items = items.replace("('", "")
items = items.replace("',", ":")
items = items.replace(")", "")
items = items.replace("{", "\n{")

gate_str = ", ".join(
[f"'{gate_name}': {count}" for gate_name, count in self.gate_types.items()]
)
items += "\ngate_types:\n{" + gate_str + "}"
return items

def _ipython_display_(self):
"""Displays __str__ in ipython instead of __repr__"""
print(str(self))


def _count_resources(tape: QuantumTape, shots: int) -> Resources:
def _count_resources(tape, shots: int) -> Resources:
"""Given a quantum circuit (tape) and number of samples, this function
counts the resources used by standard PennyLane operations.
Expand Down
42 changes: 25 additions & 17 deletions pennylane/tape/qscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,38 +1143,46 @@ def specs(self):
dict[str, Union[defaultdict,int]]: dictionaries that contain quantum script specifications
**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.PauliZ(0) @ qml.PauliZ(1))])
>>> 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.PauliZ(0) @ qml.PauliZ(1))])
Asking for the specs produces a dictionary as shown below:
Asking for the specs produces a dictionary of useful information about the circuit:
>>> qscript.specs['num_observables']
1
>>> qscript.specs['gate_sizes']
defaultdict(int, {1: 4, 2: 2})
>>> qscript.specs['gate_types']
defaultdict(int, {'Hadamard': 2, 'RX': 1, 'CNOT': 2, 'Rot': 1})
As ``defaultdict`` objects, any key not present in the dictionary returns 0.
>>> qscript.specs['gate_types']['RZ']
0
defaultdict(<class 'int'>, {1: 4, 2: 2})
>>> print(qscript.specs['resources'])
wires: 2
gates: 6
depth: 4
shots: 0
gate_types:
{'Hadamard': 2, 'RX': 1, 'CNOT': 2, 'Rot': 1}
"""
if self._specs is None:
self._specs = {"gate_sizes": defaultdict(int), "gate_types": defaultdict(int)}
resources = qml.resource.resource._count_resources(
self, shots=0
) # pylint: disable=protected-access

self._specs = {
"resources": resources,
"gate_sizes": defaultdict(int),
"gate_types": defaultdict(int),
}

for op in self.operations:
# don't use op.num_wires to allow for flexible gate classes like QubitUnitary
self._specs["gate_sizes"][len(op.wires)] += 1
self._specs["gate_types"][op.name] += 1

self._specs["num_operations"] = len(self.operations)
self._specs["num_operations"] = resources.num_gates
self._specs["num_observables"] = len(self.observables)
self._specs["num_diagonalizing_gates"] = len(self.diagonalizing_gates)
self._specs["num_used_wires"] = self.num_wires
self._specs["depth"] = self.graph.get_depth()
self._specs["num_trainable_params"] = self.num_params
self._specs["depth"] = resources.depth

return self._specs

Expand Down
13 changes: 11 additions & 2 deletions tests/legacy/test_qscript_old.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,14 +430,17 @@ def test_empty_qs_specs(self):
assert qs.specs["gate_sizes"] == defaultdict(int)
assert qs.specs["gate_types"] == defaultdict(int)

gate_types = defaultdict(int)
assert qs.specs["resources"] == qml.resource.Resources(gate_types=gate_types)

assert qs.specs["num_operations"] == 0
assert qs.specs["num_observables"] == 0
assert qs.specs["num_diagonalizing_gates"] == 0
assert qs.specs["num_used_wires"] == 0
assert qs.specs["num_trainable_params"] == 0
assert qs.specs["depth"] == 0

assert len(qs.specs) == 8
assert len(qs.specs) == 9

assert qs._specs is qs.specs

Expand All @@ -449,10 +452,16 @@ def test_specs_tape(self, make_script):
specs = qs.specs
assert qs._specs is specs

assert len(specs) == 8
assert len(specs) == 9

assert specs["gate_sizes"] == defaultdict(int, {1: 3, 2: 1})
assert specs["gate_types"] == defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})

gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})
assert specs["resources"] == qml.resource.Resources(
num_wires=3, num_gates=4, gate_types=gate_types, depth=3
)

assert specs["num_operations"] == 4
assert specs["num_observables"] == 2
assert specs["num_diagonalizing_gates"] == 1
Expand Down
15 changes: 8 additions & 7 deletions tests/resource/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Test base Resource class and its associated methods
"""
from dataclasses import FrozenInstanceError
from collections import defaultdict
import pytest

import pennylane as qml
Expand All @@ -29,14 +30,14 @@ class TestResources:
resource_quantities = (
Resources(),
Resources(5, 0, {}, 0, 0),
Resources(1, 3, {"Hadamard": 1, "PauliZ": 2}, 3, 10),
Resources(1, 3, defaultdict(int, {"Hadamard": 1, "PauliZ": 2}), 3, 10),
Resources(4, 2, {"Hadamard": 1, "CNOT": 1}, 2, 100),
)

resource_parameters = (
(0, 0, {}, 0, 0),
(5, 0, {}, 0, 0),
(1, 3, {"Hadamard": 1, "PauliZ": 2}, 3, 10),
(1, 3, defaultdict(int, {"Hadamard": 1, "PauliZ": 2}), 3, 10),
(4, 2, {"Hadamard": 1, "CNOT": 1}, 2, 100),
)

Expand All @@ -61,22 +62,22 @@ def test_set_attributes_error(self):
setattr(r, attr_name, 1)

test_str_data = (
("wires: 0\n" + "gates: 0\n" + "depth: 0\n" + "shots: 0\n" + "gate_types: \n" + "{}"),
("wires: 5\n" + "gates: 0\n" + "depth: 0\n" + "shots: 0\n" + "gate_types: \n" + "{}"),
("wires: 0\n" + "gates: 0\n" + "depth: 0\n" + "shots: 0\n" + "gate_types:\n" + "{}"),
("wires: 5\n" + "gates: 0\n" + "depth: 0\n" + "shots: 0\n" + "gate_types:\n" + "{}"),
(
"wires: 1\n"
+ "gates: 3\n"
+ "depth: 3\n"
+ "shots: 10\n"
+ "gate_types: \n"
+ "gate_types:\n"
+ "{'Hadamard': 1, 'PauliZ': 2}"
),
(
"wires: 4\n"
+ "gates: 2\n"
+ "depth: 2\n"
+ "shots: 100\n"
+ "gate_types: \n"
+ "gate_types:\n"
+ "{'Hadamard': 1, 'CNOT': 1}"
),
)
Expand All @@ -89,7 +90,7 @@ def test_str(self, r, rep):
test_rep_data = (
"Resources(num_wires=0, num_gates=0, gate_types={}, depth=0, shots=0)",
"Resources(num_wires=5, num_gates=0, gate_types={}, depth=0, shots=0)",
"Resources(num_wires=1, num_gates=3, gate_types={'Hadamard': 1, 'PauliZ': 2}, "
"Resources(num_wires=1, num_gates=3, gate_types=defaultdict(<class 'int'>, {'Hadamard': 1, 'PauliZ': 2}), "
"depth=3, shots=10)",
"Resources(num_wires=4, num_gates=2, gate_types={'Hadamard': 1, 'CNOT': 1}, "
"depth=2, shots=100)",
Expand Down
11 changes: 9 additions & 2 deletions tests/tape/test_qscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ def test_empty_qs_specs(self):
qs = QuantumScript()
assert qs._specs is None

assert qs.specs["resources"] == qml.resource.Resources()
assert qs.specs["gate_sizes"] == defaultdict(int)
assert qs.specs["gate_types"] == defaultdict(int)

Expand All @@ -430,7 +431,7 @@ def test_empty_qs_specs(self):
assert qs.specs["num_trainable_params"] == 0
assert qs.specs["depth"] == 0

assert len(qs.specs) == 8
assert len(qs.specs) == 9

assert qs._specs is qs.specs

Expand All @@ -442,7 +443,13 @@ def test_specs_tape(self, make_script):
specs = qs.specs
assert qs._specs is specs

assert len(specs) == 8
assert len(specs) == 9

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, depth=3
)
assert specs["resources"] == expected_resources

assert specs["gate_sizes"] == defaultdict(int, {1: 3, 2: 1})
assert specs["gate_types"] == defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})
Expand Down
80 changes: 28 additions & 52 deletions tests/tape/test_tape.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,22 +559,32 @@ def test_specs_empty_tape(self, make_empty_tape):
assert tape.specs["gate_sizes"] == defaultdict(int)
assert tape.specs["gate_types"] == defaultdict(int)

gate_types = defaultdict(int)
expected_resources = qml.resource.Resources(num_wires=2, gate_types=gate_types)
assert tape.specs["resources"] == expected_resources

assert tape.specs["num_operations"] == 0
assert tape.specs["num_observables"] == 1
assert tape.specs["num_diagonalizing_gates"] == 0
assert tape.specs["num_used_wires"] == 2
assert tape.specs["num_trainable_params"] == 0
assert tape.specs["depth"] == 0

assert len(tape.specs) == 8
assert len(tape.specs) == 9

def test_specs_tape(self, make_tape):
"""Tests that regular tapes return correct specifications"""
tape = make_tape

specs = tape.specs

assert len(specs) == 8
assert len(specs) == 9

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, depth=3
)
assert specs["resources"] == expected_resources

assert specs["gate_sizes"] == defaultdict(int, {1: 3, 2: 1})
assert specs["gate_types"] == defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})
Expand All @@ -591,9 +601,16 @@ def test_specs_add_to_tape(self, make_extendible_tape):
tape = make_extendible_tape
specs1 = tape.specs

assert len(specs1) == 8
assert len(specs1) == 9
assert specs1["gate_sizes"] == defaultdict(int, {1: 3, 2: 1})
assert specs1["gate_types"] == defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 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, depth=3
)
assert specs1["resources"] == expected_resoures

assert specs1["num_operations"] == 4
assert specs1["num_observables"] == 0
assert specs1["num_diagonalizing_gates"] == 0
Expand All @@ -609,64 +626,23 @@ def test_specs_add_to_tape(self, make_extendible_tape):

specs2 = tape.specs

assert len(specs2) == 8
assert len(specs2) == 9
assert specs2["gate_sizes"] == defaultdict(int, {1: 4, 2: 2})
assert specs2["gate_types"] == defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 2, "RZ": 1})

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, depth=4
)
assert specs2["resources"] == expected_resoures

assert specs2["num_operations"] == 6
assert specs2["num_observables"] == 2
assert specs2["num_diagonalizing_gates"] == 1
assert specs2["num_used_wires"] == 5
assert specs2["num_trainable_params"] == 6
assert specs2["depth"] == 4

def test_resources_empty_tape(self, make_empty_tape):
"""Test that empty tapes return empty resource counts."""
tape = make_empty_tape

assert len(tape.specs["gate_types"]) == 0
assert tape.specs["depth"] == 0

def test_resources_tape(self, make_tape):
"""Test that regular tapes return correct number of resources."""
tape = make_tape

depth = tape.specs["depth"]
assert depth == 3

# Verify resource counts
resources = tape.specs["gate_types"]
assert len(resources) == 3
assert resources["RX"] == 2
assert resources["Rot"] == 1
assert resources["CNOT"] == 1

def test_resources_add_to_tape(self, make_extendible_tape):
"""Test that tapes return correct number of resources after adding to them."""
tape = make_extendible_tape

depth = tape.specs["depth"]
assert depth == 3

resources = tape.specs["gate_types"]
assert len(resources) == 3
assert resources["RX"] == 2
assert resources["Rot"] == 1
assert resources["CNOT"] == 1

with tape as tape:
qml.CNOT(wires=[0, 1])
qml.RZ(0.1, wires=3)
qml.expval(qml.PauliX(wires="a"))
qml.probs(wires=[0, "a"])

assert tape.specs["depth"] == 4
resources = tape.specs["gate_types"]
assert len(resources) == 4
assert resources["RX"] == 2
assert resources["Rot"] == 1
assert resources["CNOT"] == 2
assert resources["RZ"] == 1


class TestParameters:
"""Tests for parameter processing, setting, and manipulation"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def circuit(state_vector):
circuit(state_vector)
tape = spy.call_args[0][0]

assert tape.specs["gate_types"]["CNOT"] == n_CNOT
assert tape.specs["resources"].gate_types["CNOT"] == n_CNOT

def test_custom_wire_labels(self, tol):
"""Test that template can deal with non-numeric, nonconsecutive wire labels."""
Expand Down
Loading

0 comments on commit a87fca3

Please sign in to comment.