Repository: PennyLaneAI/pennylane
Pull request #1245 (2021-04-21 16:09:08)

specs transform
This PR introduces a "transform" that calculates circuit resources. The function `qml.transforms.resource_estimation` takes a qnode, and returns a function.  When that function is called with circuit arguments, it returns a dictionary of information.

```python
import pennylane as qml
from pennylane import numpy as np

n_wires = 3
dev = qml.device('default.qubit', wires=n_wires)

weights = np.array([[0.5, 0.6, 1.0]])

@qml.qnode(dev)
def circuit(weights):
    qml.templates.BasicEntanglerLayers(weights=weights, wires=range(n_wires))
    return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_wires)]

resources = qml.transforms.resource_estimation(circuit)
resources(weights)
```

```
{'dev_short_name': 'default.qubit.autograd',
 'num_wires': 3,
 'num_gates': 6,
 'num_ops_by_size': {1: 3, 2: 3}}
```

The current values are
* `dev_short_name`
* `num_wires`: the number of wires on the device
* `num_gates`: total number of gates
* `num_ops_by_size`: a dictionary that gives the number of operations of a certain size.  For example, the above circuit has three single qubit gates and three two-qubit gates.

The `qml.transforms.resource_estimation` function can take in a keyword `expand_depth`, but it looks like `qnode.construct` expanded everything anyway.

In the future, I would like to add time scaling for applicable devices given relevant circuit structure information.

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [.github/CHANGELOG.md](/PennyLaneAI/pennylane/blob/master/.github/CHANGELOG.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1245](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (358eac4) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/35ab137975927421e9db9e31304566bd2476576d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (35ab137) will **decrease** coverage by `0.14%`.
> The diff coverage is `15.78%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #1245      +/-   ##
==========================================
- Coverage   98.14%   98.00%   -0.15%     
==========================================
  Files         146      147       +1     
  Lines       11035    11054      +19     
==========================================
+ Hits        10830    10833       +3     
- Misses        205      221      +16     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/transforms/resource\_estimation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvcmVzb3VyY2VfZXN0aW1hdGlvbi5weQ==) | `11.11% <11.11%> (ø)` | |
| [pennylane/transforms/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [35ab137...358eac4](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1245?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

Thenerdstation: Yeah this looks pretty good! Now all we need is a function `tape -> {"cost": float}` that estimates the single shot cost to run on braket. 
glassnotes: (Just a fly-by comment): It might be worth absorbing the existing `tape.get_resources` and `tape.get_depth` functions into this,  since it looks like it's going to be a more general resource estimator, which is awesome!

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 249848161..949fc6d98 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -2,6 +2,50 @@
 
 <h3>New features since last release</h3>
 
+* 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 67973e660..f7371f829 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -49,6 +49,7 @@ from pennylane.transforms import (
     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 dd766a09c..d0e9a0d7e 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -720,6 +720,65 @@ class QNode:
             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 686e180cc..aa95e99ce 100644
--- a/pennylane/tape/qubit_param_shift.py
+++ b/pennylane/tape/qubit_param_shift.py
@@ -385,3 +385,68 @@ class QubitParamShiftTape(JacobianTape):
             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 a557370c5..b0dfa5a8c 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 @@ class QuantumTape(AnnotatedQueue):
 
         self._trainable_params = set()
         self._graph = None
-        self._resources = None
+        self._specs = None
         self._depth = None
         self._output_dim = 0
 
@@ -500,7 +501,7 @@ class QuantumTape(AnnotatedQueue):
     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 @@ class QuantumTape(AnnotatedQueue):
 
         >>> 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 @@ class QuantumTape(AnnotatedQueue):
 
         >>> 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 6fed603ff..688893390 100644
--- a/pennylane/transforms/__init__.py
+++ b/pennylane/transforms/__init__.py
@@ -32,6 +32,7 @@ that compute the desired quantity.
     ~transforms.classical_jacobian
     ~draw
     ~metric_tensor
+    ~specs
 
 Transforms that act on quantum functions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -81,5 +82,6 @@ from .hamiltonian_expand import hamiltonian_expand
 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 1593f52e5..74f6c69b5 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 000000000..f4994ef76
--- /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


Test patch:
diff --git a/tests/tape/test_qnode.py b/tests/tape/test_qnode.py
index aa3abe6fe..d0d10b7e5 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 @@ class TestShots:
         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 6e3ac8d60..177d47612 100644
--- a/tests/tape/test_qubit_param_shift.py
+++ b/tests/tape/test_qubit_param_shift.py
@@ -86,6 +86,28 @@ class TestGradMethod:
         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 5b78ea4da..748ef7fc4 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 @@ class TestResourceEstimation:
 
         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 @@ class TestResourceEstimation:
         """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 @@ class TestResourceEstimation:
             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 000000000..8380c2ef0
--- /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"