diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 2498481618a..949fc6d981f 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -2,6 +2,50 @@
New features since last release
+* The `specs` QNode transform creates a function that produces the specifications for a circuit
+ at given arguments and keywords. Specifications can also be viewed after execution of a QNode or
+ tape by accessing their `specs` property.
+ [(#1245)](https://github.com/PennyLaneAI/pennylane/pull/1245)
+
+ For example:
+
+ ```python
+ dev = qml.device('default.qubit', wires=4)
+
+ @qml.qnode(dev, diff_method='parameter-shift')
+ def circuit(x, y):
+ qml.RX(x[0], wires=0)
+ qml.Toffoli(wires=(0, 1, 2))
+ qml.CRY(x[1], wires=(0, 1))
+ qml.Rot(x[2], x[3], y, wires=0)
+ return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1))
+
+ x = np.array([0.05, 0.1, 0.2, 0.3], requires_grad=True)
+ y = np.array(0.4, requires_grad=False)
+
+ specs_func = qml.specs(circuit)
+ info = specs_func(x, y)
+ ```
+
+ ```pycon
+ >>> info
+ {'gate_sizes': defaultdict(int, {1: 2, 3: 1, 2: 1}),
+ 'gate_types': defaultdict(int, {'RX': 1, 'Toffoli': 1, 'CRY': 1, 'Rot': 1}),
+ 'num_operations': 4,
+ 'num_observables': 2,
+ 'num_diagonalizing_gates': 1,
+ 'num_used_wires': 3,
+ 'depth': 4,
+ 'num_trainable_params': 4,
+ 'num_parameter_shift_executions': 11,
+ 'num_device_wires': 4,
+ 'device_name': 'default.qubit',
+ 'diff_method': 'parameter-shift'}
+ ```
+
+ The tape methods `get_resources` and `get_depth` are superseded by `specs` and will be
+ deprecated after one release cycle.
+
- Math docstrings in class `QubitParamShiftTape` now rendered properly.
[(#1402)](https://github.com/PennyLaneAI/pennylane/pull/1402)
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 67973e660b2..f7371f829f8 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -49,6 +49,7 @@
ctrl,
measurement_grouping,
metric_tensor,
+ specs,
qfunc_transform,
single_tape_transform,
quantum_monte_carlo,
diff --git a/pennylane/qnode.py b/pennylane/qnode.py
index dd766a09c2b..d0e9a0d7ecc 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -720,6 +720,65 @@ def circuit():
charset=charset, wire_order=wire_order, show_all_wires=show_all_wires
)
+ @property
+ def specs(self):
+ """Resource information about a quantum circuit.
+
+ Returns:
+ dict[str, Union[defaultdict,int]]: dictionaries that contain QNode specifications
+
+ **Example**
+
+ .. code-block:: python3
+
+ dev = qml.device('default.qubit', wires=2)
+ @qml.qnode(dev)
+ def circuit(x):
+ qml.RX(x[0], wires=0)
+ qml.RY(x[1], wires=1)
+ qml.CNOT(wires=(0,1))
+ return qml.probs(wires=(0,1))
+
+ x = np.array([0.1, 0.2])
+ res = circuit(x)
+
+ >>> circuit.specs
+ {'gate_sizes': defaultdict(int, {1: 2, 2: 1}),
+ 'gate_types': defaultdict(int, {'RX': 1, 'RY': 1, 'CNOT': 1}),
+ 'num_operations': 3,
+ 'num_observables': 1,
+ 'num_diagonalizing_gates': 0,
+ 'num_used_wires': 2,
+ 'depth': 2,
+ 'num_device_wires': 2,
+ 'device_name': 'default.qubit.autograd',
+ 'diff_method': 'backprop'}
+
+ """
+ if self.qtape is None:
+ raise qml.QuantumFunctionError(
+ "The QNode specifications can only be calculated after its quantum tape has been constructed."
+ )
+
+ info = self.qtape.specs.copy()
+
+ info["num_device_wires"] = self.device.num_wires
+ info["device_name"] = self.device.short_name
+
+ # TODO: use self.diff_method when that value gets updated
+ if self.diff_method != "best":
+ info["diff_method"] = self.diff_method
+ else:
+ info["diff_method"] = self.qtape.jacobian_options["method"]
+
+ # tapes do not accurately track parameters for backprop
+ # TODO: calculate number of trainable parameters in backprop
+ # find better syntax for determining if backprop
+ if info["diff_method"] == "backprop":
+ del info["num_trainable_params"]
+
+ return info
+
def to_tf(self, dtype=None):
"""Apply the TensorFlow interface to the internal quantum tape.
diff --git a/pennylane/tape/qubit_param_shift.py b/pennylane/tape/qubit_param_shift.py
index 686e180cc26..aa95e99ceb5 100644
--- a/pennylane/tape/qubit_param_shift.py
+++ b/pennylane/tape/qubit_param_shift.py
@@ -385,3 +385,68 @@ def processing_fn(results):
return np.apply_along_axis(dot, 0, results)
return tapes, processing_fn
+
+ @property
+ def specs(self):
+ """Resource information about a quantum circuit.
+
+ Returns:
+ dict[str, Union[defaultdict,int]]: dictionaries that contain tape specifications
+
+ **Example**
+
+ .. code-block:: python3
+
+ with qml.tape.QubitParamShiftTape() as tape:
+ qml.Hadamard(wires=0)
+ qml.RZ(0.26, wires=1)
+ qml.CNOT(wires=[1, 0])
+ qml.Rot(1.8, -2.7, 0.2, wires=0)
+ qml.Hadamard(wires=1)
+ qml.CNOT(wires=[0, 1])
+ qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+ Asking for the specs produces a dictionary as shown below:
+
+ >>> tape.specs['gate_sizes']
+ defaultdict(int, {1: 4, 2: 2})
+ >>> tape.specs['gate_types']
+ defaultdict(int, {'Hadamard': 2, 'RZ': 1, 'CNOT': 2, 'Rot': 1})
+
+ As ``defaultdict`` objects, any key not present in the dictionary returns 0.
+
+ >>> tape.specs['gate_types']['RX']
+ 0
+
+ In parameter-shift tapes, the number of device executions necessary for a gradient
+ is calulated as well:
+
+ >>> tape.specs['num_parameter_shift_executions]
+ 9
+
+ """
+
+ info = super().specs
+
+ if any(m.return_type is qml.operation.State for m in self.measurements):
+ return info
+
+ if len(self._par_info) > 0:
+ if "grad_method" not in self._par_info[0]:
+ self._update_gradient_info()
+
+ # Initialize with the forward pass execution
+ num_executions = 1
+ # Loop over all variables
+ for _, grad_info in self._par_info.items():
+
+ # if this variable uses parameter-shift
+ if grad_info["grad_method"] == "A":
+
+ # get_parameter_shift returns operation specific derivative formula
+ # for ops with 4-term gradients, the array will contain 4 pieces of data.
+ num_executions += len(grad_info["op"].get_parameter_shift(grad_info["p_idx"]))
+
+ info["num_parameter_shift_executions"] = num_executions
+
+ return info
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
index a557370c5fa..b0dfa5a8cf3 100644
--- a/pennylane/tape/tape.py
+++ b/pennylane/tape/tape.py
@@ -15,10 +15,11 @@
This module contains the base quantum tape.
"""
# pylint: disable=too-many-instance-attributes,protected-access,too-many-branches,too-many-public-methods
-from collections import Counter, deque
+from collections import Counter, deque, defaultdict
import contextlib
import copy
from threading import RLock
+import warnings
import numpy as np
@@ -317,7 +318,7 @@ def __init__(self, name=None, do_queue=True):
self._trainable_params = set()
self._graph = None
- self._resources = None
+ self._specs = None
self._depth = None
self._output_dim = 0
@@ -500,7 +501,7 @@ def _update_trainable_params(self):
def _update(self):
"""Update all internal tape metadata regarding processed operations and observables"""
self._graph = None
- self._resources = None
+ self._specs = None
self._depth = None
self._update_circuit_info()
self._update_par_info()
@@ -1002,23 +1003,23 @@ def get_resources(self):
>>> tape.get_resources()
{'Hadamard': 2, 'RZ': 1, 'CNOT': 2, 'Rot': 1}
+
"""
- if self._resources is None:
- self._resources = {}
- for op in self.operations:
- if op.name not in self._resources.keys():
- self._resources[op.name] = 1
- else:
- self._resources[op.name] += 1
+ warnings.warn(
+ "``tape.get_resources``is now deprecated and will be removed in v0.17. "
+ "Please use the more general ``tape.specs`` instead.",
+ UserWarning,
+ )
- return self._resources
+ return self.specs["gate_types"]
def get_depth(self):
"""Depth of the quantum circuit.
Returns:
- int: Circuit depth, computed as the longest path in the circuit's directed acyclic graph representation.
+ int: Circuit depth, computed as the longest path in the
+ circuit's directed acyclic graph representation.
**Example**
@@ -1036,12 +1037,70 @@ def get_depth(self):
>>> tape.get_depth()
4
+
"""
+
+ warnings.warn(
+ "``tape.get_depth`` is now deprecated and will be removed in v0.17. "
+ "Please use the more general ``tape.specs`` instead.",
+ UserWarning,
+ )
+
if self._depth is None:
self._depth = self.graph.get_depth()
return self._depth
+ @property
+ def specs(self):
+ """Resource information about a quantum circuit.
+
+ Returns:
+ dict[str, Union[defaultdict,int]]: dictionaries that contain tape specifications
+
+ **Example**
+
+ .. code-block:: python3
+
+ with qml.tape.QuantumTape() as tape:
+ qml.Hadamard(wires=0)
+ qml.RZ(0.26, wires=1)
+ qml.CNOT(wires=[1, 0])
+ qml.Rot(1.8, -2.7, 0.2, wires=0)
+ qml.Hadamard(wires=1)
+ qml.CNOT(wires=[0, 1])
+ qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+ Asking for the specs produces a dictionary as shown below:
+
+ >>> tape.specs['gate_sizes']
+ defaultdict(int, {1: 4, 2: 2})
+ >>> tape.specs['gate_types']
+ defaultdict(int, {'Hadamard': 2, 'RZ': 1, 'CNOT': 2, 'Rot': 1})
+
+ As ``defaultdict`` objects, any key not present in the dictionary returns 0.
+
+ >>> tape.specs['gate_types']['RX']
+ 0
+
+ """
+ if self._specs is None:
+ self._specs = {"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_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
+
+ return self._specs
+
def draw(self, charset="unicode", wire_order=None, show_all_wires=False):
"""Draw the quantum tape as a circuit diagram.
diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py
index 6fed603ff9a..688893390b8 100644
--- a/pennylane/transforms/__init__.py
+++ b/pennylane/transforms/__init__.py
@@ -32,6 +32,7 @@
~transforms.classical_jacobian
~draw
~metric_tensor
+ ~specs
Transforms that act on quantum functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -81,5 +82,6 @@
from .invisible import invisible
from .measurement_grouping import measurement_grouping
from .metric_tensor import metric_tensor, metric_tensor_tape
+from .specs import specs
from .qfunc_transforms import make_tape, single_tape_transform, qfunc_transform
from .qmc import quantum_monte_carlo
diff --git a/pennylane/transforms/draw.py b/pennylane/transforms/draw.py
index 1593f52e5b1..74f6c69b513 100644
--- a/pennylane/transforms/draw.py
+++ b/pennylane/transforms/draw.py
@@ -30,7 +30,7 @@ def draw(qnode, charset="unicode", wire_order=None, show_all_wires=False):
show_all_wires (bool): If True, all wires, including empty wires, are printed.
Returns:
- A function that has the same arguement signature as ``qnode``. When called,
+ A function that has the same argument signature as ``qnode``. When called,
the function will draw the QNode.
**Example**
diff --git a/pennylane/transforms/specs.py b/pennylane/transforms/specs.py
new file mode 100644
index 00000000000..f4994ef76f2
--- /dev/null
+++ b/pennylane/transforms/specs.py
@@ -0,0 +1,97 @@
+# Copyright 2018-2021 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.
+"""Code for resource estimation"""
+
+
+def specs(qnode, max_expansion=None):
+ """Resource information about a quantum circuit.
+
+ This transform converts a QNode into a callable that provides resource information
+ about the circuit.
+
+ Args:
+ qnode (.QNode): the QNode to calculate the specifications for
+
+ Keyword Args:
+ max_expansion (int): The number of times the internal circuit should be expanded when
+ calculating the specification. Defaults to ``qnode.max_expansion``.
+
+ Returns:
+ A function that has the same argument signature as ``qnode``. This function
+ returns a dictionary of information about qnode structure.
+
+ **Example**
+
+ .. code-block:: python3
+
+ x = np.array([0.1, 0.2])
+
+ dev = qml.device('default.qubit', wires=2)
+ @qml.qnode(dev)
+ def circuit(x, add_ry=True):
+ qml.RX(x[0], wires=0)
+ qml.CNOT(wires=(0,1))
+ if add_ry:
+ qml.RY(x[1], wires=1)
+ return qml.probs(wires=(0,1))
+
+ >>> qml.specs(circuit)(x, add_ry=False)
+ {'gate_sizes': defaultdict(int, {1: 1, 2: 1}),
+ 'gate_types': defaultdict(int, {'RX': 1, 'CNOT': 1}),
+ 'num_operations': 2,
+ 'num_observables': 1,
+ 'num_diagonalizing_gates': 0,
+ 'num_used_wires': 2,
+ 'depth': 2,
+ 'num_device_wires': 2,
+ 'device_name': 'default.qubit.autograd',
+ 'diff_method': 'backprop'}
+
+ """
+
+ def specs_qnode(*args, **kwargs):
+ """Returns information on the structure and makeup of provided QNode.
+
+ Dictionary keys:
+ * ``"num_operations"``
+ * ``"num_observables"``
+ * ``"num_diagonalizing_gates"``
+ * ``"gate_sizes"``: dictionary mapping gate number of wires to number of occurances
+ * ``"gate_types"``: dictionary mapping gate types to number of occurances
+ * ``"num_used_wires"``: number of wires used by the circuit
+ * ``"num_device_wires"``: number of wires in device
+ * ``"depth"``: longest path in directed acyclic graph representation
+ * ``"dev_short_name"``: name of QNode device
+ * ``"diff_method"``
+
+ Potential Additional Information:
+ * ``"num_trainable_params"``: number of individual scalars that are trainable
+ * ``"num_parameter_shift_executions"``: number of times circuit will execute when
+ calculating the derivative
+
+ Returns:
+ dict[str, Union[defaultdict,int]]: dictionaries that contain QNode specifications
+ """
+ if max_expansion is not None:
+ initial_max_expansion = qnode.max_expansion
+ qnode.max_expansion = max_expansion
+
+ qnode.construct(args, kwargs)
+
+ if max_expansion is not None:
+ qnode.max_expansion = initial_max_expansion
+
+ return qnode.specs
+
+ return specs_qnode
diff --git a/tests/tape/test_qnode.py b/tests/tape/test_qnode.py
index aa3abe6fece..d0d10b7e5a8 100644
--- a/tests/tape/test_qnode.py
+++ b/tests/tape/test_qnode.py
@@ -14,8 +14,10 @@
"""Unit tests for the QNode"""
import pytest
import numpy as np
+from collections import defaultdict
import pennylane as qml
+from pennylane import numpy as pnp
from pennylane import QNodeCollection
from pennylane import qnode, QNode
from pennylane.transforms import draw
@@ -1016,6 +1018,67 @@ def circuit(a):
assert dev.shots == 3
+class TestSpecs:
+ """Tests for the qnode property specs"""
+
+ def test_specs_error(self):
+ """Tests an error is raised if the tape is not constructed."""
+
+ dev = qml.device("default.qubit", wires=4)
+
+ @qml.qnode(dev)
+ def circuit():
+ return qml.expval(qml.PauliZ(0))
+
+ with pytest.raises(qml.QuantumFunctionError, match=r"The QNode specifications"):
+ circuit.specs
+
+ @pytest.mark.parametrize(
+ "diff_method, len_info", [("backprop", 10), ("parameter-shift", 12), ("adjoint", 11)]
+ )
+ def test_specs(self, diff_method, len_info):
+ """Tests the specs property with backprop"""
+
+ dev = qml.device("default.qubit", wires=4)
+
+ @qml.qnode(dev, diff_method=diff_method)
+ def circuit(x, y):
+ qml.RX(x[0], wires=0)
+ qml.Toffoli(wires=(0, 1, 2))
+ qml.CRY(x[1], wires=(0, 1))
+ qml.Rot(x[2], x[3], y, wires=2)
+ return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1))
+
+ x = pnp.array([0.05, 0.1, 0.2, 0.3], requires_grad=True)
+ y = pnp.array(0.1, requires_grad=False)
+
+ res = circuit(x, y)
+
+ info = circuit.specs
+
+ assert len(info) == len_info
+
+ assert info["gate_sizes"] == defaultdict(int, {1: 2, 3: 1, 2: 1})
+ assert info["gate_types"] == defaultdict(int, {"RX": 1, "Toffoli": 1, "CRY": 1, "Rot": 1})
+ assert info["num_operations"] == 4
+ assert info["num_observables"] == 2
+ assert info["num_diagonalizing_gates"] == 1
+ assert info["num_used_wires"] == 3
+ assert info["depth"] == 3
+ assert info["num_device_wires"] == 4
+
+ assert info["diff_method"] == diff_method
+
+ if diff_method == "parameter-shift":
+ assert info["num_parameter_shift_executions"] == 7
+
+ if diff_method != "backprop":
+ assert info["device_name"] == "default.qubit"
+ assert info["num_trainable_params"] == 4
+ else:
+ assert info["device_name"] == "default.qubit.autograd"
+
+
def test_finitediff_float32(tol):
"""Tests that float32 parameters do not effect order 1 finite-diff results.
diff --git a/tests/tape/test_qubit_param_shift.py b/tests/tape/test_qubit_param_shift.py
index 6e3ac8d60c1..177d4761233 100644
--- a/tests/tape/test_qubit_param_shift.py
+++ b/tests/tape/test_qubit_param_shift.py
@@ -86,6 +86,28 @@ def test_finite_diff(self, monkeypatch):
assert tape._grad_method(2) == "A"
+def test_specs_num_parameter_shift_executions():
+ """Tests specs has the correct number of parameter-shift executions"""
+
+ dev = qml.device("default.qubit", wires=3)
+ x = 0.543
+ y = -0.654
+
+ with qml.tape.QubitParamShiftTape() as tape:
+ qml.CRX(x, wires=[0, 1])
+ qml.RY(y, wires=[1])
+ qml.CNOT(wires=[0, 1])
+ qml.RY(0.12345, wires=2)
+ qml.expval(qml.PauliZ(0) @ qml.PauliX(1))
+
+ num_exec = tape.specs["num_parameter_shift_executions"]
+ assert num_exec == 7
+
+ jac = tape.jacobian(dev)
+
+ assert num_exec == (dev.num_executions + 1)
+
+
class TestParameterShiftRule:
"""Tests for the parameter shift implementation"""
diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py
index 5b78ea4da9c..748ef7fc471 100644
--- a/tests/tape/test_tape.py
+++ b/tests/tape/test_tape.py
@@ -13,6 +13,8 @@
# limitations under the License.
"""Unit tests for the QuantumTape"""
import copy
+import warnings
+from collections import defaultdict
import numpy as np
import pytest
@@ -343,21 +345,96 @@ def make_extendible_tape(self):
return tape
+ def test_specs_empty_tape(self, make_empty_tape):
+ """Test specs attribute on an empty tape"""
+ tape = make_empty_tape
+
+ assert tape.specs["gate_sizes"] == defaultdict(int)
+ assert tape.specs["gate_types"] == defaultdict(int)
+
+ 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
+
+ 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 specs["gate_sizes"] == defaultdict(int, {1: 3, 2: 1})
+ assert specs["gate_types"] == defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})
+ assert specs["num_operations"] == 4
+ assert specs["num_observables"] == 2
+ assert specs["num_diagonalizing_gates"] == 1
+ assert specs["num_used_wires"] == 3
+ assert specs["num_trainable_params"] == 5
+ assert specs["depth"] == 3
+
+ def test_specs_add_to_tape(self, make_extendible_tape):
+ """Test that tapes return correct specs after adding to them."""
+
+ tape = make_extendible_tape
+ specs1 = tape.specs
+
+ assert len(specs1) == 8
+ assert specs1["gate_sizes"] == defaultdict(int, {1: 3, 2: 1})
+ assert specs1["gate_types"] == defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1})
+ assert specs1["num_operations"] == 4
+ assert specs1["num_observables"] == 0
+ assert specs1["num_diagonalizing_gates"] == 0
+ assert specs1["num_used_wires"] == 3
+ assert specs1["num_trainable_params"] == 5
+ assert specs1["depth"] == 3
+
+ 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"])
+
+ specs2 = tape.specs
+
+ assert len(specs2) == 8
+ assert specs2["gate_sizes"] == defaultdict(int, {1: 4, 2: 2})
+ assert specs2["gate_types"] == defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 2, "RZ": 1})
+ 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 tape.get_depth() == 0
- assert len(tape.get_resources()) == 0
+ with pytest.warns(UserWarning, match=r"``tape.get_resources``is now deprecated"):
+ info = tape.get_resources()
+ assert len(info) == 0
+
+ with pytest.warns(UserWarning, match=r"``tape.get_depth`` is now deprecated"):
+ depth = tape.get_depth()
+ assert depth == 0
def test_resources_tape(self, make_tape):
"""Test that regular tapes return correct number of resources."""
tape = make_tape
- assert tape.get_depth() == 3
+ with pytest.warns(UserWarning, match=r"``tape.get_depth`` is now deprecated"):
+ depth = tape.get_depth()
+ assert depth == 3
# Verify resource counts
- resources = tape.get_resources()
+ with pytest.warns(UserWarning, match=r"``tape.get_resources``is now deprecated"):
+ resources = tape.get_resources()
assert len(resources) == 3
assert resources["RX"] == 2
assert resources["Rot"] == 1
@@ -367,9 +444,12 @@ 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
- assert tape.get_depth() == 3
+ with pytest.warns(UserWarning, match=r"``tape.get_depth`` is now deprecated"):
+ depth = tape.get_depth()
+ assert depth == 3
- resources = tape.get_resources()
+ with pytest.warns(UserWarning, match=r"``tape.get_resources``is now deprecated"):
+ resources = tape.get_resources()
assert len(resources) == 3
assert resources["RX"] == 2
assert resources["Rot"] == 1
@@ -381,9 +461,11 @@ def test_resources_add_to_tape(self, make_extendible_tape):
qml.expval(qml.PauliX(wires="a"))
qml.probs(wires=[0, "a"])
- assert tape.get_depth() == 4
+ with pytest.warns(UserWarning, match=r"``tape.get_depth`` is now deprecated"):
+ assert tape.get_depth() == 4
- resources = tape.get_resources()
+ with pytest.warns(UserWarning, match=r"``tape.get_resources``is now deprecated"):
+ resources = tape.get_resources()
assert len(resources) == 4
assert resources["RX"] == 2
assert resources["Rot"] == 1
diff --git a/tests/transforms/test_specs.py b/tests/transforms/test_specs.py
new file mode 100644
index 00000000000..8380c2ef095
--- /dev/null
+++ b/tests/transforms/test_specs.py
@@ -0,0 +1,165 @@
+# Copyright 2018-2021 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.
+"""Unit tests for the specs transform"""
+import pytest
+from collections import defaultdict
+
+import pennylane as qml
+from pennylane import numpy as np
+
+
+class TestSpecsTransform:
+ """Tests for the transform specs"""
+
+ @pytest.mark.parametrize(
+ "diff_method, len_info", [("backprop", 10), ("parameter-shift", 12), ("adjoint", 11)]
+ )
+ def test_empty(self, diff_method, len_info):
+
+ dev = qml.device("default.qubit", wires=1)
+
+ @qml.qnode(dev, diff_method=diff_method)
+ def circ():
+ return qml.expval(qml.PauliZ(0))
+
+ info_func = qml.specs(circ)
+ info = info_func()
+
+ circ()
+ assert info == circ.specs
+ assert len(info) == len_info
+
+ assert info["gate_sizes"] == defaultdict(int)
+ assert info["gate_types"] == defaultdict(int)
+ assert info["num_observables"] == 1
+ assert info["num_operations"] == 0
+ assert info["num_diagonalizing_gates"] == 0
+ assert info["num_used_wires"] == 1
+ assert info["depth"] == 0
+ assert info["num_device_wires"] == 1
+ assert info["diff_method"] == diff_method
+
+ if diff_method == "parameter-shift":
+ assert info["num_parameter_shift_executions"] == 1
+
+ if diff_method != "backprop":
+ assert info["device_name"] == "default.qubit"
+ assert info["num_trainable_params"] == 0
+ else:
+ assert info["device_name"] == "default.qubit.autograd"
+
+ @pytest.mark.parametrize(
+ "diff_method, len_info", [("backprop", 10), ("parameter-shift", 12), ("adjoint", 11)]
+ )
+ def test_specs(self, diff_method, len_info):
+ """Test the specs transforms works in standard situations"""
+ dev = qml.device("default.qubit", wires=4)
+
+ @qml.qnode(dev, diff_method=diff_method)
+ def circuit(x, y, add_RY=True):
+ qml.RX(x[0], wires=0)
+ qml.Toffoli(wires=(0, 1, 2))
+ qml.CRY(x[1], wires=(0, 1))
+ qml.Rot(x[2], x[3], y, wires=2)
+ if add_RY:
+ qml.RY(x[4], wires=1)
+ return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1))
+
+ x = np.array([0.05, 0.1, 0.2, 0.3, 0.5], requires_grad=True)
+ y = np.array(0.1, requires_grad=False)
+
+ info_func = qml.specs(circuit)
+
+ info = info_func(x, y, add_RY=False)
+
+ circuit(x, y, add_RY=False)
+
+ assert info == circuit.specs
+
+ assert len(info) == len_info
+
+ assert info["gate_sizes"] == defaultdict(int, {1: 2, 3: 1, 2: 1})
+ assert info["gate_types"] == defaultdict(int, {"RX": 1, "Toffoli": 1, "CRY": 1, "Rot": 1})
+ assert info["num_operations"] == 4
+ assert info["num_observables"] == 2
+ assert info["num_diagonalizing_gates"] == 1
+ assert info["num_used_wires"] == 3
+ assert info["depth"] == 3
+ assert info["num_device_wires"] == 4
+ assert info["diff_method"] == diff_method
+
+ if diff_method == "parameter-shift":
+ assert info["num_parameter_shift_executions"] == 7
+
+ if diff_method != "backprop":
+ assert info["device_name"] == "default.qubit"
+ assert info["num_trainable_params"] == 4
+ else:
+ assert info["device_name"] == "default.qubit.autograd"
+
+ @pytest.mark.parametrize(
+ "diff_method, len_info", [("backprop", 10), ("parameter-shift", 11), ("adjoint", 11)]
+ )
+ def test_specs_state(self, diff_method, len_info):
+ """Test specs works when state returned"""
+
+ dev = qml.device("default.qubit", wires=2)
+
+ @qml.qnode(dev, diff_method=diff_method)
+ def circuit():
+ return qml.state()
+
+ info_func = qml.specs(circuit)
+ info = info_func()
+
+ circuit()
+ assert info == circuit.specs
+ assert len(info) == len_info
+
+ assert info["num_observables"] == 1
+ assert info["num_diagonalizing_gates"] == 0
+
+ def test_max_expansion(self):
+ """Test that a user can calculation specifications for a different max
+ expansion parameter."""
+
+ n_layers = 2
+ n_wires = 5
+
+ dev = qml.device("default.qubit", wires=n_wires)
+
+ @qml.qnode(dev)
+ def circuit(params):
+ qml.templates.BasicEntanglerLayers(params, wires=range(n_wires))
+ return qml.expval(qml.PauliZ(0))
+
+ params_shape = qml.templates.BasicEntanglerLayers.shape(n_layers=n_layers, n_wires=n_wires)
+ rng = np.random.default_rng(seed=10)
+ params = rng.standard_normal(params_shape)
+
+ assert circuit.max_expansion == 10
+ info = qml.specs(circuit, max_expansion=0)(params)
+ assert circuit.max_expansion == 10
+
+ assert len(info) == 10
+
+ assert info["gate_sizes"] == defaultdict(int, {5: 1})
+ assert info["gate_types"] == defaultdict(int, {"BasicEntanglerLayers": 1})
+ assert info["num_operations"] == 1
+ assert info["num_observables"] == 1
+ assert info["num_used_wires"] == 5
+ assert info["depth"] == 1
+ assert info["num_device_wires"] == 5
+ assert info["device_name"] == "default.qubit.autograd"
+ assert info["diff_method"] == "backprop"