Repository: PennyLaneAI/pennylane

Issue:
Demos and examples
More demos should be added (in the examples directory). They should be self-contained Python scripts that use OpenQML to do something interesting and educational. The demos should have lots of comments in them, ideally explaining both what we are doing, and why we are doing it.

Ideas:

* Training a circuit to produce/approximate a state with given properties
* Training a quantum classifier circuit
 


Discussion:
Added two working examples on VQEs (need to be adapted once final API is out)
Added one pseudocode example on CV quantum neural net.

There are 3 qubit and 2 CV examples on branch examples_maria. I will try to code up a CV GAN to have 6 in total.

However, none of the examples except one does what it is supposed to do and I think I need some help here. 

Tut Q1 - works
Tut Q2 - does not work with project Q backend
Tut Q3 - does not work with regularizer (see #56), cost INCREASES, classifier always seems to predict either all -1 or all 1

Tut CV1 - gradient is always zero, although it clearly should not be
Tut CV2 - does not learn much, although Juan Miguel's code in tensorflow with exactly the same settings learns nicely.

I have an inkling that we have a mix of bugs here.


You can use the .py file for convenience, or the jupyter notebook.
Does Tut Q2 work with the default.qubit backend? That's odd.

For Tut CV1, that sounds like the same issue that currently causes photon redirection to fail, which likely won't be fixed until @smite merges his next feature branch.

So definitely a mix of bugs!
Yes it seems to work with the default backend. Can the redirection bug be also responsible for the CV2 to fail?

I am going through T2 to see why it doesn't work with ProjectQ.

The first problem is definitely the preparation with `QubitStateVector`. This is not a bug in the plugin but a "shortcoming" of ProjectQ. They don't know how to decompose the manually specified state into an elementary gate sequence that would prepare it. I don't think I can do anything about it. We could use a manually specified gate sequence instead of the initial state given as a vector. They do know ho to decompose arbitrary single qubit initial states.

For example, if I simply use `qm.Hadamard([0])` instead of `qm.QubitStateVector(initial_state, wires=[0, 1])` in `ansatz()`. ProjectQ produces the same result for the optimization than the default plugin.

Maybe it is also good to not start with the weights initialized to zero? For some initial states I get stuck with both the default and the ProjectQ plugin. People might modify the example when they play around and get frustrated...
Waiting to update the examples when the new structure is out. I think Josh wanted to put the examples into the plugin repo and only keep two very minimal default templates for openqml itself. We can then make sure the ProjectQ examples do not use QubiStateVector hacks.
[This is more for myself as reminder]

Testing the examples with latest merge. Issues:

- np and onp?

PQ:
1 Different behaviour of ProjectQ and default.qubit - PQ gets stuck on saddle point!
3a - Test state prep script. Does not learn.  [KWARGS BUG]
3b - Does not learn  [KWARGS BUG]

SF:
3 QNN Does not learn [KWARGS BUG]

I just realised that we're missing a *Tutorials* section in the docs sidebar, as we have with Strawberry Fields. I've added that in the latest master, it just needs to be populated now.

@mariaschuld, we could kill two birds with one stone, and embed the Jupyter notebooks for the examples directly in the docs? See https://strawberryfields.readthedocs.io/en/latest/gallery/state_learner/StateLearning.html for an example of what this looks like.

The advantages of this approach are:
* it is a bit of a time saver
* it is easier to directly include images and plots
* it is easier to quickly see that the code is correct and runs
* I can auto-insert a link at the bottom for users to download the notebook of the tutorial.

Downsides, however, are
* Since Jupyter notebook uses markdown rather than ReST, you sometimes need to be really vigilant that it is formatting quickly once it has been converted to display in the docs.
* Likewise, cross-referencing between notebooks and ReST pages is slightly stricter.
* The sphinx compilation stage is significantly slower.

The docs for Notebook/Spinx plugin is available here: https://nbsphinx.readthedocs.io/en/0.3.5/
The alternative would be to convert the notebook to a rst file and make it static, right?

Will the docs tutorial section be distinguished into the different plugins? Is it not confusing that one needs different repos to run the tutorials?

Had a quick conversation with @mariaschuld, this was the consensus:

1. OpenQML shouldn't depend on any of the plugins, to avoid pulling in libraries the user might not want.

2. The docs should have a page(under 'Getting Started') that lists all 'official' plugins, and describes the plugin ecosystem for the user (i.e. OpenQML is designed to be used with plugins, these need to be pip installed, etc).

3. All tutorials for the 'official' plugins should be in the main OpenQML docs, with a disclaimer at the top (i.e. `.. note::`  or `.. warning::` or `.. info::`) which says

   > To run this tutorial, you must install `pennylane-sf` via `pip install pennylane-sf`'

   This keeps everything centralised (for the user) and easier to keep track of (for us).

4. The actual plugins will have their own docs, but bare bones and *just* to describe their API via Sphinx. These will be linked in the main OpenQML docs, and vice versa.
Just checking in on the status of examples. 

- As stated above, we want all examples/tutorials to be placed in the main library. This is so that users can find them easiest (it will not affect the dependencies of the library). We can also have copies in other repos, but we don't want them to be missing from the core docs

- So far there are two tutorials ("Basic tutorial: qubit rotation" and "Photon redirection and hybrid computation") present in the main library documentation. These look great! (one todo still to take care of)

- I added a QGAN example last night (worked right away, really cool :) ), but it'll need a bit more polishing

- @mariaschuld where can we find the remaining examples/tutorials? Can we get these placed in the core library `master` branch now?
Sure. My last bit of information was that we put them into the documentation of the main library but in the repo of the plugins to not to have code for plugins in the main repo. But we broke this rule anyways in the mini-demos by using SF for the Fock State preparation.  

I can easily move the notebooks and .py files to the main repo. My suggestion is that I do this after the 3 PRs are merged, to avoid lots of work merging.
Examples are on add_examples... branch in the main repo and refactored.

Last things to do:
- TutQ3: stateprep sign bug 
- Test TutCVs (currently not sure how to load plugin)
All running

Resolving patch:
diff --git a/pennylane/measurements.py b/pennylane/measurements.py
--- a/pennylane/measurements.py
+++ b/pennylane/measurements.py
@@ -233,6 +233,9 @@ def shape(self, device=None):
             QuantumFunctionError: the return type of the measurement process is
                 unrecognized and cannot deduce the numeric type
         """
+        if qml.active_return():
+            return self._shape_new(device=device)
+
         shape = None
 
         # First: prepare the shape for return types that do not require a
@@ -288,6 +291,85 @@ def shape(self, device=None):
             + f"{self.return_type}."
         )
 
+    @functools.lru_cache()
+    def _shape_new(self, device=None):
+        """The expected output shape of the MeasurementProcess.
+
+        Note that the output shape is dependent on the device when:
+
+        * The ``return_type`` is either ``Probability``, ``State`` (from :func:`.state`) or
+          ``Sample``;
+        * The shot vector was defined in the device.
+
+        For example, assuming a device with ``shots=None``, expectation values
+        and variances define ``shape=(,)``, whereas probabilities in the qubit
+        model define ``shape=(2**num_wires)`` where ``num_wires`` is the
+        number of wires the measurement acts on.
+
+        Args:
+            device (.Device): a PennyLane device to use for determining the
+                shape
+
+        Returns:
+            tuple: the output shape
+
+        Raises:
+            QuantumFunctionError: the return type of the measurement process is
+                unrecognized and cannot deduce the numeric type
+        """
+        shape = None
+
+        # First: prepare the shape for return types that do not require a
+        # device
+        if self.return_type in (Expectation, MutualInfo, Variance, VnEntropy):
+            shape = ()
+
+        density_matrix_return = self.return_type == State and self.wires
+
+        if density_matrix_return:
+            dim = 2 ** len(self.wires)
+            shape = (dim, dim)
+
+        # Determine shape if device with shot vector
+        if device is not None and device._shot_vector is not None:
+            shape = self._shot_vector_shape(device, main_shape=shape)
+
+        # If we have a shape, return it here
+        if shape is not None:
+            return shape
+
+        # Then: handle return types that require a device; no shot vector
+        if device is None and self.return_type in (Probability, State, Sample):
+            raise MeasurementShapeError(
+                "The device argument is required to obtain the shape of the measurement process; "
+                + f"got return type {self.return_type}."
+            )
+
+        if self.return_type == Probability:
+            len_wires = len(self.wires)
+            dim = self._get_num_basis_states(len_wires, device)
+            return (dim,)
+
+        if self.return_type == State:
+            # Note: qml.density_matrix has its shape defined, so we're handling
+            # the qml.state case; acts on all device wires
+            dim = 2 ** len(device.wires)
+            return (dim,)
+
+        if self.return_type == Sample:
+            if self.obs is not None:
+                # qml.sample(some_observable) case
+                return () if device.shots == 1 else (device.shots,)
+
+            # qml.sample() case
+            len_wires = len(device.wires)
+            return (len_wires,) if device.shots == 1 else (device.shots, len_wires)
+
+        raise qml.QuantumFunctionError(
+            "Cannot deduce the shape of the measurement process with unrecognized return_type "
+            + f"{self.return_type}."
+        )
+
     @functools.lru_cache()
     def _shot_vector_shape(self, device, main_shape=None):
         """Auxiliary function for getting the output shape when the device has
@@ -296,6 +378,9 @@ def _shot_vector_shape(self, device, main_shape=None):
         The shape is device dependent even if the return type has a main shape
         pre-defined (e.g., expectation values, states, etc.).
         """
+        if qml.active_return():
+            return self._shot_vector_shape_new(device, main_shape=main_shape)
+
         shot_vector = device._shot_vector
         # pylint: disable=consider-using-generator
         num_shot_elements = sum([s.copies for s in shot_vector])
@@ -337,6 +422,47 @@ def _shot_vector_shape(self, device, main_shape=None):
 
         return shape
 
+    @functools.lru_cache()
+    def _shot_vector_shape_new(self, device, main_shape=None):
+        """Auxiliary function for getting the output shape when the device has
+        the shot vector defined.
+
+        The shape is device dependent even if the return type has a main shape
+        pre-defined (e.g., expectation values, states, etc.).
+        """
+        shot_vector = device._shot_vector
+        # pylint: disable=consider-using-generator
+        num_shot_elements = sum([s.copies for s in shot_vector])
+        shape = ()
+
+        if main_shape is not None:
+            shape = tuple(main_shape for _ in range(num_shot_elements))
+
+        elif self.return_type == qml.measurements.Probability:
+            dim = self._get_num_basis_states(len(self.wires), device)
+            shape = tuple((dim,) for _ in range(num_shot_elements))
+
+        elif self.return_type == qml.measurements.Sample:
+            if self.obs is not None:
+                shape = tuple(
+                    (shot_val,) if shot_val != 1 else tuple()
+                    for shot_val in device._raw_shot_sequence
+                )
+            else:
+                shape = tuple(
+                    (shot_val, len(device.wires)) if shot_val != 1 else (len(device.wires),)
+                    for shot_val in device._raw_shot_sequence
+                )
+
+        elif self.return_type == qml.measurements.State:
+
+            # Note: qml.density_matrix has its shape defined, so we're handling
+            # the qml.state case; acts on all device wires
+            dim = 2 ** len(device.wires)
+            shape = tuple((dim,) for _ in range(num_shot_elements))
+
+        return shape
+
     @staticmethod
     @functools.lru_cache()
     def _get_num_basis_states(num_wires, device):
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
--- a/pennylane/tape/tape.py
+++ b/pennylane/tape/tape.py
@@ -1180,6 +1180,9 @@ def func(a):
             >>> tape.shape(dev)
             (1, 4)
         """
+        if qml.active_return():
+            return self._shape_new(device)
+
         output_shape = tuple()
 
         if len(self._measurements) == 1:
@@ -1194,6 +1197,67 @@ def func(a):
                 )
         return output_shape
 
+    def _shape_new(self, device):
+        """Produces the output shape of the tape by inspecting its measurements
+        and the device used for execution.
+
+        .. note::
+
+            The computed shape is not stored because the output shape may be
+            dependent on the device used for execution.
+
+        Args:
+            device (.Device): the device that will be used for the tape execution
+
+        Returns:
+            Union[tuple[int], tuple[tuple[int]]]: the output shape(s) of the
+            tape result
+
+        **Examples**
+
+        .. code-block:: python
+
+            dev = qml.device("default.qubit", wires=2)
+            a = np.array([0.1, 0.2, 0.3])
+
+            def qfunc():
+                qml.RY(a[0], wires=0)
+                qml.RX(a[1], wires=0)
+                qml.RY(a[2], wires=0)
+
+            with qml.tape.QuantumTape() as tape:
+                qfunc()
+                qml.state()
+
+        .. code-block:: pycon
+
+            >>> tape.shape(dev)
+            (4,)
+
+        .. code-block:: python
+
+            with qml.tape.QuantumTape() as tape:
+                qfunc()
+                qml.state()
+                qml.expval(qml.PauliZ(wires=0))
+                qml.probs(wires=[0, 1])
+
+        .. code-block:: pycon
+
+            >>> tape.shape(dev)
+            ((4,), (), (4,))
+        """
+        shapes = tuple(meas_process.shape(device) for meas_process in self._measurements)
+
+        if len(shapes) == 1:
+            return shapes[0]
+
+        if device.shot_vector is not None:
+            # put the shot vector axis before the measurement axis
+            shapes = tuple(zip(*shapes))
+
+        return shapes
+
     @property
     def numeric_type(self):
         """Returns the expected numeric type of the tape result by inspecting
@@ -1228,6 +1292,9 @@ def func(a):
             >>> tape.numeric_type
             complex
         """
+        if qml.active_return():
+            return self._numeric_type_new
+
         measurement_types = {meas.return_type for meas in self._measurements}
         if len(measurement_types) > 1:
             raise TapeError(
@@ -1245,6 +1312,43 @@ def func(a):
 
         return self._measurements[0].numeric_type
 
+    @property
+    def _numeric_type_new(self):
+        """Returns the expected numeric type of the tape result by inspecting
+        its measurements.
+
+        Returns:
+            Union[type, Tuple[type]]: The numeric type corresponding to the result type of the
+            tape, or a tuple of such types if the tape contains multiple measurements
+
+        **Example:**
+
+        .. code-block:: python
+
+            dev = qml.device("default.qubit", wires=2)
+            a = np.array([0.1, 0.2, 0.3])
+
+            def func(a):
+                qml.RY(a[0], wires=0)
+                qml.RX(a[1], wires=0)
+                qml.RY(a[2], wires=0)
+
+            with qml.tape.QuantumTape() as tape:
+                func(a)
+                qml.state()
+
+        .. code-block:: pycon
+
+            >>> tape.numeric_type
+            complex
+        """
+        types = tuple(observable.numeric_type for observable in self._measurements)
+
+        if len(types) == 1:
+            return types[0]
+
+        return types
+
     def unwrap(self):
         """A context manager that unwraps a tape with tensor-like parameters
         to NumPy arrays.


Test patch:
diff --git a/tests/returntypes/test_new_tape.py b/tests/returntypes/test_new_tape.py
new file mode 100644
--- /dev/null
+++ b/tests/returntypes/test_new_tape.py
@@ -0,0 +1,534 @@
+# Copyright 2018-2022 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 QuantumTape"""
+import copy
+import warnings
+from collections import defaultdict
+
+import numpy as np
+import pytest
+from this import d
+
+import pennylane as qml
+from pennylane import CircuitGraph
+from pennylane.measurements import (
+    MeasurementProcess,
+    MeasurementShapeError,
+    counts,
+    expval,
+    sample,
+    var,
+    probs,
+)
+from pennylane.tape import QuantumTape, TapeError
+
+
+measures = [
+    (qml.expval(qml.PauliZ(0)), ()),
+    (qml.var(qml.PauliZ(0)), ()),
+    (qml.probs(wires=[0]), (2,)),
+    (qml.probs(wires=[0, 1]), (4,)),
+    (qml.state(), (8,)),  # Assumes 3-qubit device
+    (qml.density_matrix(wires=[0, 1]), (4, 4)),
+    (
+        qml.sample(qml.PauliZ(0)),
+        None,
+    ),  # Shape is None because the expected shape is in the test case
+    (qml.sample(), None),  # Shape is None because the expected shape is in the test case
+]
+
+multi_measurements = [
+    ([qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))], ((), ())),
+    ([qml.probs(wires=[0]), qml.probs(wires=[1])], ((2,), (2,))),
+    ([qml.probs(wires=[0]), qml.probs(wires=[1, 2])], ((2,), (4,))),
+    ([qml.probs(wires=[0, 2]), qml.probs(wires=[1])], ((4,), (2,))),
+    (
+        [qml.probs(wires=[0]), qml.probs(wires=[1, 2]), qml.probs(wires=[0, 1, 2])],
+        ((2,), (4,), (8,)),
+    ),
+]
+
+
+class TestMeasurementProcess:
+    """Tests for the shape and numeric type of a measurement process"""
+
+    measurements_no_shots = [
+        (qml.expval(qml.PauliZ(0)), ()),
+        (qml.var(qml.PauliZ(0)), ()),
+        (qml.probs(wires=[0, 1]), (4,)),
+        (qml.state(), (8,)),
+        (qml.density_matrix(wires=[0, 1]), (4, 4)),
+    ]
+
+    measurements_finite_shots = [
+        (qml.expval(qml.PauliZ(0)), ()),
+        (qml.var(qml.PauliZ(0)), ()),
+        (qml.probs(wires=[0, 1]), (4,)),
+        (qml.state(), (8,)),
+        (qml.density_matrix(wires=[0, 1]), (4, 4)),
+        (qml.sample(qml.PauliZ(0)), (10,)),
+        (qml.sample(), (10, 3)),
+    ]
+
+    measurements_shot_vector = [
+        (qml.expval(qml.PauliZ(0)), ((), (), ())),
+        (qml.var(qml.PauliZ(0)), ((), (), ())),
+        (qml.probs(wires=[0, 1]), ((4,), (4,), (4,))),
+        (qml.state(), ((8,), (8,), (8,))),
+        (qml.density_matrix(wires=[0, 1]), ((4, 4), (4, 4), (4, 4))),
+        (qml.sample(qml.PauliZ(0)), ((10,), (20,), (30,))),
+        (qml.sample(), ((10, 3), (20, 3), (30, 3))),
+    ]
+
+    @pytest.mark.parametrize("measurement, expected_shape", measurements_no_shots)
+    def test_output_shapes_no_shots(self, measurement, expected_shape):
+        """Test that the output shape of the measurement process is expected
+        when shots=None"""
+        num_wires = 3
+        dev = qml.device("default.qubit", wires=num_wires, shots=None)
+
+        assert measurement.shape(dev) == expected_shape
+
+    @pytest.mark.parametrize("measurement, expected_shape", measurements_finite_shots)
+    def test_output_shapes_finite_shots(self, measurement, expected_shape):
+        """Test that the output shape of the measurement process is expected
+        when shots is finite"""
+        num_wires = 3
+        num_shots = 10
+        dev = qml.device("default.qubit", wires=num_wires, shots=num_shots)
+
+        assert measurement.shape(dev) == expected_shape
+
+    @pytest.mark.parametrize("measurement, expected_shape", measurements_shot_vector)
+    def test_output_shapes_no_shots(self, measurement, expected_shape):
+        """Test that the output shape of the measurement process is expected
+        when shots is a vector"""
+        num_wires = 3
+        shot_vector = [10, 20, 30]
+        dev = qml.device("default.qubit", wires=num_wires, shots=shot_vector)
+
+        assert measurement.shape(dev) == expected_shape
+
+    @pytest.mark.parametrize("measurement", [qml.probs(wires=[0, 1]), qml.state(), qml.sample()])
+    def test_no_device_error(self, measurement):
+        """Test that an error is raised when a measurement that requires a device
+        is called without a device"""
+        msg = "The device argument is required to obtain the shape of the measurement process"
+
+        with pytest.raises(MeasurementShapeError, match=msg):
+            measurement.shape()
+
+    def test_undefined_shape_error(self):
+        """Test that an error is raised for a measurement with an undefined shape"""
+        measurement = qml.counts(wires=[0, 1])
+        msg = "Cannot deduce the shape of the measurement process with unrecognized return_type"
+
+        with pytest.raises(qml.QuantumFunctionError, match=msg):
+            measurement.shape()
+
+
+class TestOutputShape:
+    """Tests for determining the tape output shape of tapes."""
+
+    @pytest.mark.parametrize("measurement, expected_shape", measures)
+    @pytest.mark.parametrize("shots", [None, 1, 10])
+    def test_output_shapes_single(self, measurement, expected_shape, shots):
+        """Test that the output shape produced by the tape matches the expected
+        output shape."""
+        if shots is None and measurement.return_type is qml.measurements.Sample:
+            pytest.skip("Sample doesn't support analytic computations.")
+
+        num_wires = 3
+        dev = qml.device("default.qubit", wires=num_wires, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            qml.apply(measurement)
+
+        shot_dim = shots if not isinstance(shots, tuple) else len(shots)
+        if expected_shape is None:
+            expected_shape = shot_dim if shot_dim == 1 else (shot_dim,)
+
+        if measurement.return_type is qml.measurements.Sample:
+            if measurement.obs is not None:
+                expected_shape = () if shots == 1 else (shots,)
+            else:
+                expected_shape = (num_wires,) if shots == 1 else (shots, num_wires)
+
+        assert tape.shape(dev) == expected_shape
+
+    @pytest.mark.parametrize("measurement, expected_shape", measures)
+    @pytest.mark.parametrize("shots", [None, 1, 10, (1, 2, 5, 3)])
+    def test_output_shapes_single_qnode_check(self, measurement, expected_shape, shots):
+        """Test that the output shape produced by the tape matches the output
+        shape of a QNode for a single measurement."""
+        if shots is None and measurement.return_type is qml.measurements.Sample:
+            pytest.skip("Sample doesn't support analytic computations.")
+
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            qml.apply(measurement)
+
+        if shots is not None and measurement.return_type is qml.measurements.State:
+            # this is allowed by the tape but raises a warning
+            with pytest.warns(
+                UserWarning, match="Requested state or density matrix with finite shots"
+            ):
+                res = qml.execute([tape], dev, gradient_fn=None)[0]
+        else:
+            # TODO: test gradient_fn is not None when the interface `execute` functions are implemented
+            res = qml.execute([tape], dev, gradient_fn=None)[0]
+
+        if isinstance(shots, tuple):
+            res_shape = tuple(r.shape for r in res)
+        else:
+            res_shape = res.shape
+
+        assert tape.shape(dev) == res_shape
+
+    def test_output_shapes_single_qnode_check_cutoff(self):
+        """Test that the tape output shape is correct when computing
+        probabilities with a dummy device that defines a cutoff value."""
+
+        class CustomDevice(qml.QubitDevice):
+            """A dummy device that has a cutoff value specified and returns
+            analytic probabilities in a fashion similar to the
+            strawberryfields.fock device.
+
+            Note: this device definition is used as PennyLane-SF is not a
+            dependency of PennyLane core and there are no CV device in
+            PennyLane core using a cutoff value.
+            """
+
+            name = "Device with cutoff"
+            short_name = "dummy.device"
+            pennylane_requires = "0.1.0"
+            version = "0.0.1"
+            author = "CV quantum"
+
+            operations = {}
+            observables = {"Identity"}
+
+            def __init__(self, shots=None, wires=None, cutoff=None):
+                super().__init__(wires=wires, shots=shots)
+                self.cutoff = cutoff
+
+            def apply(self, operations, **kwargs):
+                pass
+
+            def analytic_probability(self, wires=None):
+                if wires is None:
+                    wires = self.wires
+                return np.zeros(self.cutoff ** len(wires))
+
+        dev = CustomDevice(wires=2, cutoff=13)
+
+        # If PennyLane-SF is installed, the following can be checked e.g., locally:
+        # dev = qml.device("strawberryfields.fock", wires=2, cutoff_dim=13)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.probs(wires=[0])
+
+        @qml.qnode(dev)
+        def circuit(a, b):
+            return qml.probs(wires=[0])
+
+        res_shape = qml.execute([tape], dev, gradient_fn=None)[0]
+        assert tape.shape(dev) == res_shape.shape
+
+    @pytest.mark.autograd
+    @pytest.mark.parametrize("measurements, expected", multi_measurements)
+    @pytest.mark.parametrize("shots", [None, 1, 10])
+    def test_multi_measure(self, measurements, expected, shots):
+        """Test that the expected output shape is obtained when using multiple
+        expectation value, variance and probability measurements."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            for m in measurements:
+                qml.apply(m)
+
+        if measurements[0].return_type is qml.measurements.Sample:
+            expected[1] = shots
+            expected = tuple(expected)
+
+        res = tape.shape(dev)
+        assert res == expected
+
+        # TODO: test gradient_fn is not None when the interface `execute` functions are implemented
+        res = qml.execute([tape], dev, gradient_fn=None)[0]
+        res_shape = tuple(r.shape for r in res)
+
+        assert res_shape == expected
+
+    @pytest.mark.autograd
+    @pytest.mark.parametrize("measurements, expected", multi_measurements)
+    def test_multi_measure_shot_vector(self, measurements, expected):
+        """Test that the expected output shape is obtained when using multiple
+        expectation value, variance and probability measurements with a shot
+        vector."""
+        if measurements[0].return_type is qml.measurements.Probability:
+            num_wires = set(len(m.wires) for m in measurements)
+            if len(num_wires) > 1:
+                pytest.skip(
+                    "Multi-probs with varying number of varies when using a shot vector is to be updated in PennyLane."
+                )
+
+        shots = (1, 1, 3, 3, 5, 1)
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            for m in measurements:
+                qml.apply(m)
+
+        # Update expected as we're using a shotvector
+        expected = tuple(expected for _ in shots)
+        res = tape.shape(dev)
+        assert res == expected
+
+        # TODO: test gradient_fn is not None when the interface `execute` functions are implemented
+        res = qml.execute([tape], dev, gradient_fn=None)[0]
+        res_shape = tuple(tuple(r_.shape for r_ in r) for r in res)
+
+        assert res_shape == expected
+
+    @pytest.mark.autograd
+    @pytest.mark.parametrize("shots", [1, 10])
+    def test_multi_measure_sample(self, shots):
+        """Test that the expected output shape is obtained when using multiple
+        qml.sample measurements."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        num_samples = 3
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            for i in range(num_samples):
+                qml.sample(qml.PauliZ(i))
+
+        expected = tuple(() if shots == 1 else (shots,) for _ in range(num_samples))
+
+        res = tape.shape(dev)
+        assert res == expected
+
+        res = qml.execute([tape], dev, gradient_fn=None)[0]
+        res_shape = tuple(r.shape for r in res)
+
+        assert res_shape == expected
+
+    @pytest.mark.autograd
+    def test_multi_measure_sample_obs_shot_vector(self):
+        """Test that the expected output shape is obtained when using multiple
+        qml.sample measurements with an observable with a shot vector."""
+        shots = (1, 1, 3, 3, 5, 1)
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        num_samples = 3
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            for i in range(num_samples):
+                qml.sample(qml.PauliZ(i))
+
+        expected = tuple(tuple(() if s == 1 else (s,) for _ in range(num_samples)) for s in shots)
+
+        res = tape.shape(dev)
+        assert res == expected
+
+        expected = qml.execute([tape], dev, gradient_fn=None)[0]
+        expected_shape = tuple(tuple(e_.shape for e_ in e) for e in expected)
+
+        assert res == expected_shape
+
+    @pytest.mark.autograd
+    def test_multi_measure_sample_wires_shot_vector(self):
+        """Test that the expected output shape is obtained when using multiple
+        qml.sample measurements with wires with a shot vector."""
+        shots = (1, 1, 3, 3, 5, 1)
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        num_samples = 3
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(0.3, wires=0)
+            qml.RX(0.2, wires=0)
+            for i in range(num_samples):
+                qml.sample()
+
+        expected = tuple(
+            tuple((3,) if s == 1 else (s, 3) for _ in range(num_samples)) for s in shots
+        )
+
+        res = tape.shape(dev)
+        assert res == expected
+
+        expected = qml.execute([tape], dev, gradient_fn=None)[0]
+        expected_shape = tuple(tuple(e_.shape for e_ in e) for e in expected)
+
+        assert res == expected_shape
+
+
+class TestNumericType:
+    """Tests for determining the numeric type of the tape output."""
+
+    @pytest.mark.parametrize(
+        "ret", [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(0)), qml.probs(wires=[0])]
+    )
+    @pytest.mark.parametrize("shots", [None, 1, (1, 2, 3)])
+    def test_float_measures(self, ret, shots):
+        """Test that most measurements output floating point values and that
+        the tape output domain correctly identifies this."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a, b = 0.3, 0.2
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=[0])
+            qml.RZ(b, wires=[0])
+            qml.apply(ret)
+
+        result = qml.execute([tape], dev, gradient_fn=None)[0]
+        if not isinstance(result, tuple):
+            result = (result,)
+
+        # Double-check the domain of the QNode output
+        assert all(np.issubdtype(res.dtype, float) for res in result)
+        assert tape.numeric_type is float
+
+    @pytest.mark.parametrize(
+        "ret", [qml.state(), qml.density_matrix(wires=[0, 1]), qml.density_matrix(wires=[2, 0])]
+    )
+    def test_complex_state(self, ret):
+        """Test that a tape with qml.state correctly determines that the output
+        domain will be complex."""
+        dev = qml.device("default.qubit", wires=3)
+
+        a, b = 0.3, 0.2
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=[0])
+            qml.RZ(b, wires=[0])
+            qml.apply(ret)
+
+        result = qml.execute([tape], dev, gradient_fn=None)[0]
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result.dtype, complex)
+        assert tape.numeric_type is complex
+
+    @pytest.mark.parametrize("ret", [qml.sample(), qml.sample(qml.PauliZ(wires=0))])
+    def test_sample_int_eigvals(self, ret):
+        """Test that the tape can correctly determine the output domain for a
+        sampling measurement with a Hermitian observable with integer
+        eigenvalues."""
+        dev = qml.device("default.qubit", wires=3, shots=5)
+
+        arr = np.array(
+            [
+                1.32,
+                2.312,
+            ]
+        )
+        herm = np.outer(arr, arr)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(0.4, wires=[0])
+            qml.apply(ret)
+
+        result = qml.execute([tape], dev, gradient_fn=None)[0]
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result.dtype, np.int64)
+        assert tape.numeric_type is int
+
+    # TODO: add cases for each interface once qml.Hermitian supports other
+    # interfaces
+    def test_sample_real_eigvals(self):
+        """Test that the tape can correctly determine the output domain when
+        sampling a Hermitian observable with real eigenvalues."""
+        dev = qml.device("default.qubit", wires=3, shots=5)
+
+        arr = np.array(
+            [
+                1.32,
+                2.312,
+            ]
+        )
+        herm = np.outer(arr, arr)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(0.4, wires=[0])
+            qml.sample(qml.Hermitian(herm, wires=0))
+
+        result = qml.execute([tape], dev, gradient_fn=None)[0]
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result.dtype, float)
+        assert tape.numeric_type is float
+
+    @pytest.mark.autograd
+    def test_sample_real_and_int_eigvals(self):
+        """Test that the tape can correctly determine the output domain for
+        multiple sampling measurements with a Hermitian observable with real
+        eigenvalues and another one with integer eigenvalues."""
+        dev = qml.device("default.qubit", wires=3, shots=5)
+
+        arr = np.array(
+            [
+                1.32,
+                2.312,
+            ]
+        )
+        herm = np.outer(arr, arr)
+
+        a, b = 0, 3
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            qml.sample(qml.Hermitian(herm, wires=0)), qml.sample(qml.PauliZ(1))
+
+        result = qml.execute([tape], dev, gradient_fn=None)[0]
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result[0].dtype, float)
+        assert np.issubdtype(result[1].dtype, np.int64)
+        assert tape.numeric_type == (float, int)