Repository: PennyLaneAI/pennylane
Pull request #5927 (2024-07-02 13:50:12)

Legacy device facade class
**Context:**

We currently have to maintain logic for interacting with two separate device interfaces.  This adds extra maintainence, and creates a lot more paths where things can go wrong or get confusing.

Given the number of low-priority plugins that follow the legacy device interface that we probably won't get around to updating, we probably won't be able to get rid of the legacy device interface any time soon.  But we can isolate it and limit it's scope.

**Description of the Change:**

Adds a `LegacyDeviceFacade` class that maps a device from the old device interface to the new device interface.



**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**


[sc-58353]

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.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://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5927?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: Patch coverage is `89.32039%` with `22 lines` in your changes missing coverage. Please review.
> Project coverage is 99.61%. Comparing base [(`e74c9bf`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/e74c9bf821518196ce3bc1c8a740d63179cef9c3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`900fa35`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/900fa35c70fc5a01f52345ddc4cece30673a4b3d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 2 commits behind head on master.

| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5927?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/devices/legacy\_facade.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5927?src=pr&el=tree&filepath=pennylane%2Fdevices%2Flegacy_facade.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvbGVnYWN5X2ZhY2FkZS5weQ==) | 89.26% | [22 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5927?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5927      +/-   ##
==========================================
- Coverage   99.67%   99.61%   -0.06%     
==========================================
  Files         425      426       +1     
  Lines       40772    40685      -87     
==========================================
- Hits        40640    40529     -111     
- Misses        132      156      +24     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5927?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 9c60329b8..a4c2b5d8d 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -33,6 +33,10 @@
 
 <h3>Improvements 🛠</h3>
 
+* `qml.devices.LegacyDeviceFacade` has been added to map the legacy devices to the new
+  device interface.
+  [(#5927)](https://github.com/PennyLaneAI/pennylane/pull/5927)
+
 * Added the `compute_sparse_matrix` method for `qml.ops.qubit.BasisStateProjector`.
   [(#5790)](https://github.com/PennyLaneAI/pennylane/pull/5790)
 
diff --git a/pennylane/debugging/snapshot.py b/pennylane/debugging/snapshot.py
index df06ffa7d..9d6facc34 100644
--- a/pennylane/debugging/snapshot.py
+++ b/pennylane/debugging/snapshot.py
@@ -25,6 +25,8 @@ from pennylane.typing import PostprocessingFn
 
 def _is_snapshot_compatible(dev):
     # The `_debugger` attribute is a good enough proxy for snapshot compatibility
+    if isinstance(dev, qml.devices.LegacyDeviceFacade):
+        return _is_snapshot_compatible(dev.target_device)
     return hasattr(dev, "_debugger")
 
 
diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py
index 475a84aac..4fab51c42 100644
--- a/pennylane/devices/__init__.py
+++ b/pennylane/devices/__init__.py
@@ -60,6 +60,7 @@ accessible from the ``pennylane.devices`` submodule.
     DefaultTensor
     NullQubit
     DefaultQutritMixed
+    LegacyDeviceFacade
 
 Preprocessing Transforms
 ------------------------
@@ -152,6 +153,7 @@ from .execution_config import ExecutionConfig, DefaultExecutionConfig, MCMConfig
 from .device_constructor import device, refresh_devices
 from .device_api import Device
 from .default_qubit import DefaultQubit
+from .legacy_facade import LegacyDeviceFacade
 
 # DefaultQubitTF and DefaultQubitAutograd not imported here since this
 # would lead to an automatic import of tensorflow and autograd, which are
diff --git a/pennylane/devices/legacy_facade.py b/pennylane/devices/legacy_facade.py
new file mode 100644
index 000000000..8121b1ed3
--- /dev/null
+++ b/pennylane/devices/legacy_facade.py
@@ -0,0 +1,440 @@
+# Copyright 2018-2024 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.
+"""
+Defines a LegacyDeviceFacade class for converting legacy devices to the
+new interface.
+"""
+import warnings
+
+# pylint: disable=not-callable
+from contextlib import contextmanager
+from dataclasses import replace
+
+import pennylane as qml
+from pennylane.measurements import Shots
+from pennylane.transforms.core.transform_program import TransformProgram
+
+from .default_qubit import adjoint_observables, adjoint_ops
+from .device_api import Device
+from .execution_config import DefaultExecutionConfig
+from .modifiers import single_tape_support
+from .preprocess import (
+    decompose,
+    no_sampling,
+    validate_adjoint_trainable_params,
+    validate_measurements,
+    validate_observables,
+)
+
+
+@contextmanager
+def _set_shots(device, shots):
+    """Context manager to temporarily change the shots
+    of a device.
+
+    This context manager can be used in two ways.
+
+    As a standard context manager:
+
+    >>> dev = qml.device("default.qubit.legacy", wires=2, shots=None)
+    >>> with _set_shots(dev, shots=100):
+    ...     print(dev.shots)
+    100
+    >>> print(dev.shots)
+    None
+
+    Or as a decorator that acts on a function that uses the device:
+
+    >>> _set_shots(dev, shots=100)(lambda: dev.shots)()
+    100
+    """
+    # note, this function duplicates qml.workflow.set_shots
+    # duplicated here to avoid circular dependency issues
+    # qml.workflow.set_shots can be independently deprecated soon
+    # this version of the function is private to LegacyDeviceFacade
+    shots = qml.measurements.Shots(shots)
+    shots = shots.shot_vector if shots.has_partitioned_shots else shots.total_shots
+    if shots == device.shots:
+        yield
+        return
+
+    original_shots = device.shots
+    original_shot_vector = device._shot_vector  # pylint: disable=protected-access
+
+    try:
+        device.shots = shots
+        yield
+    finally:
+        device.shots = original_shots
+        device._shot_vector = original_shot_vector  # pylint: disable=protected-access
+
+
+def null_postprocessing(results):
+    """A postprocessing function with null behavior."""
+    return results[0]
+
+
+@qml.transform
+def legacy_device_expand_fn(tape, device):
+    """Turn the ``expand_fn`` from the legacy device interface into a transform."""
+    new_tape = _set_shots(device, tape.shots)(device.expand_fn)(tape)
+    return (new_tape,), null_postprocessing
+
+
+@qml.transform
+def legacy_device_batch_transform(tape, device):
+    """Turn the ``batch_transform`` from the legacy device interface into a transform."""
+    return _set_shots(device, tape.shots)(device.batch_transform)(tape)
+
+
+def _add_adjoint_transforms(program: TransformProgram, name="adjoint"):
+    """Add the adjoint specific transforms to the transform program."""
+    program.add_transform(no_sampling, name=name)
+    program.add_transform(
+        decompose,
+        stopping_condition=adjoint_ops,
+        name=name,
+    )
+    program.add_transform(validate_observables, adjoint_observables, name=name)
+    program.add_transform(
+        validate_measurements,
+        name=name,
+    )
+    program.add_transform(qml.transforms.broadcast_expand)
+    program.add_transform(validate_adjoint_trainable_params)
+
+
+@single_tape_support
+class LegacyDeviceFacade(Device):
+    """
+    A Facade that converts a device from the old ``qml.Device`` interface into the new interface.
+
+    Args:
+        device (qml.device.LegacyDevice): a device that follows the legacy device interface.
+
+    >>> from pennylane.devices import DefaultMixed, LegacyDeviceFacade
+    >>> legacy_dev = DefaultMixed(wires=2)
+    >>> new_dev = LegacyDeviceFacade(dev)
+    >>> new_dev.preprocess()
+    (TransformProgram(legacy_device_batch_transform, legacy_device_expand_fn, defer_measurements),
+    ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None,
+    gradient_method=None, gradient_keyword_arguments={}, device_options={}, interface=None,
+    derivative_order=1, mcm_config=MCMConfig(mcm_method=None, postselect_mode=None)))
+    >>> new_dev.shots
+    Shots(total_shots=None, shot_vector=())
+    >>> tape = qml.tape.QuantumScript([], [qml.sample(wires=0)], shots=5)
+    >>> new_dev.execute(tape)
+    array([0., 0., 0., 0., 0.])
+
+    """
+
+    # pylint: disable=super-init-not-called
+    def __init__(self, device: "qml.devices.LegacyDevice"):
+        if not isinstance(device, qml.devices.LegacyDevice):
+            raise ValueError(
+                "The LegacyDeviceFacade only accepts a device of type qml.devices.LegacyDevice."
+            )
+        self._device = device
+
+    @property
+    def tracker(self):
+        """A :class:`~.Tracker` that can store information about device executions, shots, batches,
+        intermediate results, or any additional device dependent information.
+        """
+        return self._device.tracker
+
+    @tracker.setter
+    def tracker(self, new_tracker):
+        self._device.tracker = new_tracker
+
+    @property
+    def name(self) -> str:
+        return self._device.short_name
+
+    def __repr__(self):
+        return f"<LegacyDeviceFacade: {repr(self._device)}>"
+
+    def __getattr__(self, name):
+        return getattr(self._device, name)
+
+    @property
+    def target_device(self) -> "qml._device.Device":
+        """The device wrapped by the facade."""
+        return self._device
+
+    @property
+    def wires(self) -> qml.wires.Wires:
+        return self._device.wires
+
+    # pylint: disable=protected-access
+    @property
+    def shots(self) -> Shots:
+        if self._device._shot_vector:
+            return Shots(self._device._raw_shot_sequence)
+        return Shots(self._device.shots)
+
+    @property
+    def _debugger(self):
+        return self._device._debugger
+
+    @_debugger.setter
+    def _debugger(self, new_debugger):
+        self._device._debugger = new_debugger
+
+    def preprocess(self, execution_config=DefaultExecutionConfig):
+        execution_config = self._setup_execution_config(execution_config)
+        program = qml.transforms.core.TransformProgram()
+        # note: need to wrap these methods with a set_shots modifier
+        program.add_transform(legacy_device_batch_transform, device=self._device)
+        program.add_transform(legacy_device_expand_fn, device=self._device)
+        if execution_config.gradient_method == "adjoint":
+            _add_adjoint_transforms(program, name=f"{self.name} + adjoint")
+
+        if not self._device.capabilities().get("supports_mid_measure", False):
+            program.add_transform(qml.defer_measurements, device=self)
+
+        return program, execution_config
+
+    def _setup_backprop_config(self, execution_config):
+        tape = qml.tape.QuantumScript()
+        if not self._validate_backprop_method(tape):
+            raise qml.DeviceError("device does not support backprop.")
+        if execution_config.use_device_gradient is None:
+            return replace(execution_config, use_device_gradient=True)
+        return execution_config
+
+    def _setup_adjoint_config(self, execution_config):
+        tape = qml.tape.QuantumScript([], [])
+        if not self._validate_adjoint_method(tape):
+            raise qml.DeviceError("device does not support device derivatives")
+        updated_values = {
+            "gradient_keyword_arguments": {"use_device_state": True, "method": "adjoint_jacobian"}
+        }
+
+        if execution_config.use_device_gradient is None:
+            updated_values["use_device_gradient"] = True
+        if execution_config.grad_on_execution is None:
+            updated_values["grad_on_execution"] = True
+        return replace(execution_config, **updated_values)
+
+    def _setup_device_config(self, execution_config):
+        tape = qml.tape.QuantumScript([], [])
+        if not self._validate_device_method(tape):
+            raise qml.DeviceError("device does not support device derivatives")
+        updated_values = {}
+        if execution_config.use_device_gradient is None:
+            updated_values["use_device_gradient"] = True
+        if execution_config.grad_on_execution is None:
+            updated_values["grad_on_execution"] = True
+        return replace(execution_config, **updated_values)
+
+    # pylint: disable=too-many-return-statements
+    def _setup_execution_config(self, execution_config):
+        if execution_config.gradient_method == "best":
+            tape = qml.tape.QuantumScript([], [])
+            if self._validate_backprop_method(tape):
+                config = replace(execution_config, gradient_method="backprop")
+                return self._setup_backprop_config(config)
+            if self._validate_adjoint_method(tape):
+                config = replace(execution_config, gradient_method="adjoint")
+                return self._setup_adjoint_config(config)
+            if self._validate_device_method(tape):
+                config = replace(execution_config, gradient_method="device")
+                return self._setup_execution_config(config)
+
+        if execution_config.gradient_method == "backprop":
+            return self._setup_backprop_config(execution_config)
+        if execution_config.gradient_method == "adjoint":
+            return self._setup_adjoint_config(execution_config)
+        if execution_config.gradient_method == "device":
+            return self._setup_device_config(execution_config)
+
+        return execution_config
+
+    def supports_derivatives(self, execution_config=None, circuit=None) -> bool:
+        circuit = qml.tape.QuantumScript([], [], shots=self.shots) if circuit is None else circuit
+
+        if execution_config is None or execution_config.gradient_method == "best":
+            validation_methods = (
+                self._validate_backprop_method,
+                self._validate_adjoint_method,
+                self._validate_device_method,
+            )
+            return any(validate(circuit) for validate in validation_methods)
+
+        if execution_config.gradient_method == "backprop":
+            return self._validate_backprop_method(circuit)
+        if execution_config.gradient_method == "adjoint":
+            return self._validate_adjoint_method(circuit)
+        if execution_config.gradient_method == "device":
+            return self._validate_device_method(circuit)
+        return False
+
+    # pylint: disable=protected-access
+    def _create_temp_device(self, batch):
+        """Create a temporary device for use in a backprop execution."""
+        params = []
+        for t in batch:
+            params.extend(t.get_parameters(trainable_only=False))
+        interface = qml.math.get_interface(*params)
+        if interface == "numpy":
+            return self._device
+
+        mapped_interface = qml.workflow.execution.INTERFACE_MAP.get(interface, interface)
+
+        backprop_interface = self._device.capabilities().get("passthru_interface", None)
+        if mapped_interface == backprop_interface:
+            return self._device
+
+        backprop_devices = self._device.capabilities().get("passthru_devices", None)
+
+        if backprop_devices is None:
+            raise qml.DeviceError(f"Device {self} does not support backpropagation.")
+
+        if backprop_devices[mapped_interface] == self._device.short_name:
+            return self._device
+
+        if self.target_device.short_name != "default.qubit.legacy":
+            warnings.warn(
+                "The switching of devices for backpropagation is now deprecated in v0.38 and "
+                "will be removed in v0.39, as this behavior was developed purely for the "
+                "deprecated default.qubit.legacy.",
+                qml.PennyLaneDeprecationWarning,
+            )
+
+        # create new backprop device
+        expand_fn = self._device.expand_fn
+        batch_transform = self._device.batch_transform
+        if hasattr(self._device, "_debugger"):
+            debugger = self._device._debugger
+        else:
+            debugger = "No debugger"
+        tracker = self._device.tracker
+
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                action="ignore",
+                category=qml.PennyLaneDeprecationWarning,
+                message=r"use 'default.qubit'",
+            )
+            # we already warned about backprop device switching
+            new_device = qml.device(
+                backprop_devices[mapped_interface],
+                wires=self._device.wires,
+                shots=self._device.shots,
+            )
+
+        new_device.expand_fn = expand_fn
+        new_device.batch_transform = batch_transform
+        if debugger != "No debugger":
+            new_device._debugger = debugger
+        new_device.tracker = tracker
+
+        return new_device
+
+    # pylint: disable=protected-access
+    def _update_original_device(self, temp_device):
+        """After performing an execution with a backprop device, update the state of the original device."""
+        # Update for state vector simulators that have the _pre_rotated_state attribute
+        if hasattr(self._device, "_pre_rotated_state"):
+            self._device._pre_rotated_state = temp_device._pre_rotated_state
+
+        # Update for state vector simulators that have the _state attribute
+        if hasattr(self._device, "_state"):
+            self._device._state = temp_device._state
+
+    def _validate_backprop_method(self, tape):
+        if tape.shots:
+            return False
+        params = tape.get_parameters(trainable_only=False)
+        interface = qml.math.get_interface(*params)
+
+        if tape and any(isinstance(m.obs, qml.SparseHamiltonian) for m in tape.measurements):
+            return False
+        if interface == "numpy":
+            interface = None
+        mapped_interface = qml.workflow.execution.INTERFACE_MAP.get(interface, interface)
+
+        # determine if the device supports backpropagation
+        backprop_interface = self._device.capabilities().get("passthru_interface", None)
+        if backprop_interface is not None:
+            # device supports backpropagation natively
+            return mapped_interface in [backprop_interface, "Numpy"]
+        # determine if the device has any child devices that support backpropagation
+        backprop_devices = self._device.capabilities().get("passthru_devices", None)
+
+        if backprop_devices is None:
+            return False
+        return mapped_interface in backprop_devices or mapped_interface == "Numpy"
+
+    def _validate_adjoint_method(self, tape):
+        # The conditions below provide a minimal set of requirements that we can likely improve upon in
+        # future, or alternatively summarize within a single device capability. Moreover, we also
+        # need to inspect the circuit measurements to ensure only expectation values are taken. This
+        # cannot be done here since we don't yet know the composition of the circuit.
+
+        required_attrs = ["_apply_operation", "_apply_unitary", "adjoint_jacobian"]
+        supported_device = all(hasattr(self._device, attr) for attr in required_attrs)
+        supported_device = supported_device and self._device.capabilities().get("returns_state")
+
+        if not supported_device:
+            return False
+        return not bool(tape.shots)
+
+    def _validate_device_method(self, _):
+        # determine if the device provides its own jacobian method
+        return self._device.capabilities().get("provides_jacobian", False)
+
+    def execute(self, circuits, execution_config=DefaultExecutionConfig):
+        dev = (
+            self._create_temp_device(circuits)
+            if execution_config.gradient_method == "backprop"
+            else self._device
+        )
+
+        kwargs = {}
+        if dev.capabilities().get("supports_mid_measure", False):
+            kwargs["postselect_mode"] = execution_config.mcm_config.postselect_mode
+
+        first_shot = circuits[0].shots
+        if all(t.shots == first_shot for t in circuits):
+            results = _set_shots(dev, first_shot)(dev.batch_execute)(circuits, **kwargs)
+        else:
+            results = tuple(
+                _set_shots(dev, t.shots)(dev.batch_execute)((t,), **kwargs)[0] for t in circuits
+            )
+
+        if dev is not self._device:
+            self._update_original_device(dev)
+
+        return results
+
+    def execute_and_compute_derivatives(self, circuits, execution_config=DefaultExecutionConfig):
+        first_shot = circuits[0].shots
+        if all(t.shots == first_shot for t in circuits):
+            return _set_shots(self._device, first_shot)(self._device.execute_and_gradients)(
+                circuits, **execution_config.gradient_keyword_arguments
+            )
+        batched_res = tuple(
+            self.execute_and_compute_derivatives((c,), execution_config) for c in circuits
+        )
+        return tuple(zip(*batched_res))
+
+    def compute_derivatives(self, circuits, execution_config=DefaultExecutionConfig):
+        first_shot = circuits[0].shots
+        if all(t.shots == first_shot for t in circuits):
+            return _set_shots(self._device, first_shot)(self._device.gradients)(
+                circuits, **execution_config.gradient_keyword_arguments
+            )
+        return tuple(self.compute_derivatives((c,), execution_config) for c in circuits)


Test patch:
diff --git a/tests/devices/test_legacy_facade.py b/tests/devices/test_legacy_facade.py
new file mode 100644
index 000000000..f662d2c9d
--- /dev/null
+++ b/tests/devices/test_legacy_facade.py
@@ -0,0 +1,456 @@
+# Copyright 2018-2024 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.
+"""
+Contains unit tests for the LegacyDeviceFacade class.
+"""
+import numpy as np
+
+# pylint: disable=protected-access
+import pytest
+
+import pennylane as qml
+from pennylane.devices.default_qubit_autograd import DefaultQubitAutograd
+from pennylane.devices.execution_config import ExecutionConfig
+from pennylane.devices.legacy_facade import (
+    LegacyDeviceFacade,
+    legacy_device_batch_transform,
+    legacy_device_expand_fn,
+)
+
+
+class DummyDevice(qml.devices.LegacyDevice):
+    """A minimal device that does not do anything."""
+
+    author = "some string"
+    name = "my legacy device"
+    short_name = "something"
+    version = 0.0
+
+    observables = {"PauliX", "PauliY", "PauliZ"}
+    operations = {"Rot", "RX", "RY", "RZ", "PauliX", "PauliY", "PauliZ", "CNOT"}
+    pennylane_requires = 0.38
+
+    def reset(self):
+        pass
+
+    # pylint: disable=unused-argument
+    def apply(self, operation, wires, par):
+        return 0.0
+
+    # pylint: disable=unused-argument
+    def expval(self, observable, wires, par):
+        return 0.0
+
+
+def test_error_if_not_legacy_device():
+    """Test that a ValueError is raiuised if the target is not a legacy device."""
+
+    target = qml.devices.DefaultQubit()
+    with pytest.raises(ValueError, match="The LegacyDeviceFacade only accepts"):
+        LegacyDeviceFacade(target)
+
+
+def test_shots():
+    """Test the shots behavior of a dummy legacy device."""
+    legacy_dev = DummyDevice(shots=(100, 100))
+    dev = LegacyDeviceFacade(legacy_dev)
+
+    assert dev.shots == qml.measurements.Shots((100, 100))
+
+    assert legacy_dev._shot_vector == list(qml.measurements.Shots((100, 100)).shot_vector)
+    assert legacy_dev.shots == 200
+    assert dev.shots == qml.measurements.Shots((100, 100))
+
+
+def test_tracker():
+    """Test that tracking works with the Facade."""
+
+    dev = LegacyDeviceFacade(DummyDevice())
+
+    with qml.Tracker(dev) as tracker:
+        _ = dev.execute(qml.tape.QuantumScript([], [qml.expval(qml.Z(0))], shots=50))
+
+    assert tracker.totals == {"executions": 1, "shots": 50, "batches": 1, "batch_len": 1}
+
+
+def test_debugger():
+    """Test that snapshots still work with legacy devices."""
+
+    dev = LegacyDeviceFacade(qml.devices.DefaultQubitLegacy(wires=1))
+
+    @qml.qnode(dev)
+    def circuit():
+        qml.Snapshot()
+        qml.Hadamard(0)
+        qml.Snapshot()
+        return qml.expval(qml.Z(0))
+
+    res = qml.snapshots(circuit)()
+    assert qml.math.allclose(res[0], np.array([1, 0]))
+    assert qml.math.allclose(res[1], 1 / np.sqrt(2) * np.array([1, 1]))
+    assert qml.math.allclose(res["execution_results"], 0)
+
+
+def test_shot_distribution():
+    """Test that different numbers of shots in a batch all get executed."""
+
+    class DummyJacobianDevice(DummyDevice):
+
+        _capabilities = {"provides_jacobian": True}
+
+        def jacobian(self, circuit):  # pylint: disable=unused-argument
+            return 0
+
+    dev = LegacyDeviceFacade(DummyJacobianDevice())
+
+    tape1 = qml.tape.QuantumScript([], [qml.expval(qml.Z(0))], shots=5)
+    tape2 = qml.tape.QuantumScript([], [qml.expval(qml.Z(0))], shots=100)
+
+    with dev.tracker:
+        dev.execute((tape1, tape2))
+    assert dev.tracker.history["shots"] == [5, 100]
+
+    with dev.tracker:
+        dev.compute_derivatives((tape1, tape2))
+
+    assert dev.tracker.history["derivatives"] == [1, 1]  # two calls
+
+    with dev.tracker:
+        dev.execute_and_compute_derivatives((tape1, tape2))
+
+    assert dev.tracker.history["batches"] == [1, 1]  # broken up into multiple calls
+    assert dev.tracker.history["shots"] == [5, 100]
+    assert dev.tracker.history["derivatives"] == [1, 1]
+
+
+def test_legacy_device_expand_fn():
+    """Test that legacy_device_expand_fn expands operations to the target gateset."""
+
+    tape = qml.tape.QuantumScript([qml.X(0), qml.IsingXX(0.5, wires=(0, 1))], [qml.state()])
+
+    expected = qml.tape.QuantumScript(
+        [qml.X(0), qml.CNOT((0, 1)), qml.RX(0.5, 0), qml.CNOT((0, 1))], [qml.state()]
+    )
+    (new_tape,), fn = legacy_device_expand_fn(tape, device=DummyDevice())
+    assert qml.equal(new_tape, expected)
+    assert fn(("A",)) == "A"
+
+
+def test_legacy_device_batch_transform():
+    """Test that legacy_device_batch_transform still performs the operations that the legacy batch transform did."""
+
+    tape = qml.tape.QuantumScript([], [qml.expval(qml.X(0) + qml.Y(0))])
+    (tape1, tape2), fn = legacy_device_batch_transform(tape, device=DummyDevice())
+
+    assert qml.equal(tape1, qml.tape.QuantumScript([], [qml.expval(qml.X(0))]))
+    assert qml.equal(tape2, qml.tape.QuantumScript([], [qml.expval(qml.Y(0))]))
+    assert fn((1.0, 2.0)) == np.array(3.0)
+
+
+def test_batch_transform_supports_hamiltonian():
+    """Assert that the legacy device batch transform absorbs the shot sensitive behavior of the
+    hamiltonian expansion."""
+
+    class HamiltonianDev(DummyDevice):
+        """A device that supports a hamiltonian."""
+
+        observables = {"Hamiltonian"}
+
+    H = qml.Hamiltonian([1, 1], [qml.X(0), qml.Y(0)])
+
+    tape = qml.tape.QuantumScript([], [qml.expval(H)], shots=None)
+    batch, _ = legacy_device_batch_transform(tape, device=HamiltonianDev())
+    assert len(batch) == 1
+
+    tape = qml.tape.QuantumScript([], [qml.expval(H)], shots=50)
+    batch, _ = legacy_device_batch_transform(tape, device=HamiltonianDev())
+    assert len(batch) == 2
+
+
+def test_basic_properties():
+    """Test the basic properties of the device."""
+
+    ld = DummyDevice(wires=(0, 1))
+    d = LegacyDeviceFacade(ld)
+    assert d.target_device is ld
+    assert d.name == "something"
+    assert d.author == "some string"
+    assert d.version == 0.0
+    assert repr(d) == f"<LegacyDeviceFacade: {repr(ld)}>"
+    assert d.wires == qml.wires.Wires((0, 1))
+
+
+def test_preprocessing_program():
+    """Test the population of the preprocessing program."""
+
+    dev = DummyDevice(wires=(0, 1))
+    program, _ = LegacyDeviceFacade(dev).preprocess()
+
+    assert (
+        program[0].transform == legacy_device_batch_transform.transform
+    )  # pylint: disable=no-member
+    assert program[1].transform == legacy_device_expand_fn.transform  # pylint: disable=no-member
+    assert program[2].transform == qml.defer_measurements.transform  # pylint: disable=no-member
+
+    m0 = qml.measure(0)
+    tape = qml.tape.QuantumScript(
+        [qml.X(0), qml.IsingXX(0.5, (0, 1)), *m0.measurements],
+        [qml.expval(qml.Hamiltonian([1, 1], [qml.X(0), qml.Y(0)]))],
+        shots=50,
+    )
+
+    (tape1, tape2), fn = program((tape,))
+    expected_ops = [qml.X(0), qml.CNOT((0, 1)), qml.RX(0.5, 0), qml.CNOT((0, 1)), qml.CNOT((0, 2))]
+    assert tape1.operations == expected_ops
+    assert tape2.operations == expected_ops
+
+    assert tape1.measurements == [qml.expval(qml.X(0))]
+    assert tape2.measurements == [qml.expval(qml.Y(0))]
+    assert tape1.shots == qml.measurements.Shots(50)
+    assert tape2.shots == qml.measurements.Shots(50)
+
+    assert qml.math.allclose(fn((1.0, 2.0)), 3.0)
+
+
+def test_preprocessing_program_supports_mid_measure():
+    """Test that if the device natively supports mid measure, defer_measurements wont be applied."""
+
+    class MidMeasureDev(DummyDevice):
+        """A dummy device that supports mid circuit measurements."""
+
+        _capabilities = {"supports_mid_measure": True}
+
+    dev = MidMeasureDev()
+    program, _ = LegacyDeviceFacade(dev).preprocess()
+    assert qml.defer_measurements not in program
+
+
+@pytest.mark.parametrize("t_postselect_mode", ("hw-like", "fill-shots"))
+def test_pass_postselect_mode_to_dev(t_postselect_mode):
+    """test that postselect mode is passed to the target if it supports mid measure."""
+
+    class MidMeasureDev(DummyDevice):
+        """A dummy device that supports mid circuit measurements."""
+
+        _capabilities = {"supports_mid_measure": True}
+
+        def batch_execute(self, circuits, postselect_mode):
+            assert postselect_mode == t_postselect_mode
+            return tuple(0 for _ in circuits)
+
+    target = MidMeasureDev()
+    dev = LegacyDeviceFacade(target)
+
+    mcm_config = qml.devices.MCMConfig(postselect_mode=t_postselect_mode)
+    config = qml.devices.ExecutionConfig(mcm_config=mcm_config)
+
+    dev.execute(qml.tape.QuantumScript(), config)
+
+
+class TestGradientSupport:
+    """Test integration with various kinds of device derivatives."""
+
+    def test_no_derivatives_case(self):
+        """Test that the relevant errors are raised when the device does not support derivatives."""
+
+        dev = LegacyDeviceFacade(DummyDevice())
+
+        assert not dev.supports_derivatives()
+        assert not dev.supports_derivatives(ExecutionConfig(gradient_method="backprop"))
+        assert not dev.supports_derivatives(ExecutionConfig(gradient_method="adjoint"))
+        assert not dev.supports_derivatives(ExecutionConfig(gradient_method="device"))
+        assert not dev.supports_derivatives(ExecutionConfig(gradient_method="param_shift"))
+
+        with pytest.raises(qml.DeviceError):
+            dev.preprocess(ExecutionConfig(gradient_method="device"))
+
+        with pytest.raises(qml.DeviceError):
+            dev.preprocess(ExecutionConfig(gradient_method="adjoint"))
+
+        with pytest.raises(qml.DeviceError):
+            dev.preprocess(ExecutionConfig(gradient_method="backprop"))
+
+    @pytest.mark.parametrize("gradient_method", ("best", "adjoint"))
+    def test_adjoint_support(self, gradient_method):
+        """Test that the facade can handle devices that support adjoint."""
+
+        # pylint: disable=unnecessary-lambda-assignment
+        class AdjointDev(DummyDevice):
+            """A dummy device that supports adjoint diff"""
+
+            _capabilities = {"returns_state": True}
+
+            _apply_operation = lambda *args, **kwargs: 0
+            _apply_unitary = lambda *args, **kwargs: 0
+            adjoint_jacobian = lambda *args, **kwargs: "a"
+
+        adj_dev = AdjointDev()
+        dev = LegacyDeviceFacade(adj_dev)
+
+        tape = qml.tape.QuantumScript([], [], shots=None)
+        assert dev._validate_adjoint_method(tape)
+        tape_shots = qml.tape.QuantumScript([], [], shots=50)
+        assert not dev._validate_adjoint_method(tape_shots)
+
+        config = qml.devices.ExecutionConfig(gradient_method=gradient_method)
+        assert dev.supports_derivatives(config, tape)
+        assert not dev.supports_derivatives(config, tape_shots)
+
+        program, processed_config = dev.preprocess(config)
+        assert processed_config.use_device_gradient is True
+        assert processed_config.gradient_keyword_arguments == {
+            "use_device_state": True,
+            "method": "adjoint_jacobian",
+        }
+        assert processed_config.grad_on_execution is True
+
+        tape = qml.tape.QuantumScript(
+            [qml.Rot(qml.numpy.array(1.2), 2.3, 3.4, 0)], [qml.expval(qml.Z(0))]
+        )
+        (new_tape,), _ = program((tape,))
+        expected = qml.tape.QuantumScript(
+            [qml.RZ(qml.numpy.array(1.2), 0), qml.RY(2.3, 0), qml.RZ(3.4, 0)],
+            [qml.expval(qml.Z(0))],
+        )
+        qml.assert_equal(new_tape, expected)
+
+        out = dev.compute_derivatives(tape, processed_config)
+        assert out == "a"  # the output of adjoint_jacobian
+
+        res, jac = dev.execute_and_compute_derivatives(tape, processed_config)
+        assert qml.math.allclose(res, 0)
+        assert jac == "a"
+
+    def test_device_derivatives(self):
+        """Test that a device that provides a derivative processed the config correctly."""
+
+        class DeviceDerivatives(DummyDevice):
+            """A dummy device with a jacobian."""
+
+            _capabilities = {"provides_jacobian": True}
+
+        ddev = DeviceDerivatives()
+        dev = LegacyDeviceFacade(ddev)
+
+        assert dev.supports_derivatives()
+        assert dev.supports_derivatives(ExecutionConfig(gradient_method="device"))
+        assert dev._validate_device_method(qml.tape.QuantumScript())
+
+        config = qml.devices.ExecutionConfig(gradient_method="best")
+        _, processed_config = dev.preprocess(config)
+        assert processed_config.use_device_gradient is True
+        assert processed_config.grad_on_execution is True
+
+    def test_no_backprop_with_sparse_hamiltonian(self):
+        """Test that backpropagation is not supported with SparseHamiltonian."""
+
+        class BackpropDevice(DummyDevice):
+
+            _capabilities = {"passthru_interface": "autograd"}
+
+        H = qml.SparseHamiltonian(qml.X.compute_sparse_matrix(), wires=0)
+        x = qml.numpy.array(0.1)
+        tape = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(H)])
+        dev = LegacyDeviceFacade(BackpropDevice())
+        assert not dev.supports_derivatives(ExecutionConfig(gradient_method="backprop"), tape)
+
+        tape2 = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.Z(0))])
+        assert dev.supports_derivatives(ExecutionConfig(gradient_method="backprop"), tape2)
+
+    def test_passthru_interface_no_substitution(self):
+        """Test that if the passthru interface is set, no substitution occurs."""
+
+        class BackpropDevice(DummyDevice):
+
+            _capabilities = {"passthru_interface": "autograd"}
+
+        dev = LegacyDeviceFacade(BackpropDevice(wires=2, shots=None))
+
+        x = qml.numpy.array(0.1)
+        tape = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.Z(0))])
+
+        assert dev.supports_derivatives(qml.devices.ExecutionConfig(gradient_method="backprop"))
+        assert dev._create_temp_device((tape,)) is dev.target_device
+
+        config = qml.devices.ExecutionConfig(gradient_method="backprop", use_device_gradient=True)
+        assert dev.preprocess(config)[1] is config  # unchanged
+
+    def test_backprop_has_passthru_devices(self):
+        """Test that backprop is supported if the device has passthru devices."""
+
+        class BackpropDevice(DummyDevice):
+
+            _capabilities = {"passthru_devices": {"autograd": "default.qubit.autograd"}}
+
+        dev = LegacyDeviceFacade(BackpropDevice(shots=None))
+
+        x = qml.numpy.array(0.1)
+        tape = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.Z(0))])
+        assert dev.supports_derivatives()
+        assert dev.supports_derivatives(ExecutionConfig(gradient_method="backprop"))
+        assert dev.supports_derivatives(ExecutionConfig(gradient_method="backprop"), tape)
+
+        config = qml.devices.ExecutionConfig(gradient_method="backprop", use_device_gradient=True)
+        assert dev.preprocess(config)[1] is config  # unchanged
+
+        with pytest.warns(qml.PennyLaneDeprecationWarning, match="switching of devices"):
+            tmp_device = dev._create_temp_device((tape,))
+        assert tmp_device.short_name == "default.qubit.autograd"
+
+    def test_backprop_passthru_device_self(self):
+        """Test that the temporary device is the original device if the passthru device is itself."""
+
+        class BackpropSelfDevice(DummyDevice):
+
+            short_name = "BackpropSelfDevice"
+
+            _capabilities = {"passthru_devices": {"autograd": "BackpropSelfDevice"}}
+
+        dev = LegacyDeviceFacade(BackpropSelfDevice(wires=2))
+
+        x = qml.numpy.array(0.1)
+        tape = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.Z(0))])
+        tmp_dev = dev._create_temp_device((tape,))
+        assert tmp_dev is dev.target_device
+
+    def test_passthru_device_does_not_exist(self):
+        """Test that if backprop is requested for a device that does not support it, a device error is raised."""
+
+        x = qml.numpy.array(0.1)
+        tape = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.Z(0))])
+
+        dev = LegacyDeviceFacade(DummyDevice(wires=2))
+        config = qml.devices.ExecutionConfig(gradient_method="backprop")
+        with pytest.raises(qml.DeviceError, match=r"does not support backpropagation"):
+            dev.execute(tape, config)
+
+    @pytest.mark.parametrize("dev_class", (qml.devices.DefaultQubitLegacy, DefaultQubitAutograd))
+    def test_backprop_device_substitution(self, dev_class):
+        """Test that default.qubit.legacy is substituted for a backprop device during backprop execution."""
+
+        with pytest.warns(qml.PennyLaneDeprecationWarning, match="use 'default.qubit'"):
+            dq_legacy = dev_class(wires=2)
+        dev = LegacyDeviceFacade(dq_legacy)
+
+        def f(x):
+            tape = qml.tape.QuantumScript([qml.RX(x, 0)], [qml.expval(qml.Z(0))])
+            return dev.execute(tape, qml.devices.ExecutionConfig(gradient_method="backprop"))
+
+        assert qml.math.allclose(dq_legacy.state, np.array([1, 0, 0, 0]))
+
+        with dev.tracker:
+            g = qml.grad(f)(qml.numpy.array(0.5))
+        assert qml.math.allclose(g, -np.sin(0.5))
+        assert dev.tracker.totals["executions"] == 1
+        assert not qml.math.allclose(dq_legacy.state, np.array([1, 0, 0, 0]))