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"