Repository: PennyLaneAI/pennylane
Pull request #5693 (2024-05-15 20:52:07)

Changing `convert_to_numpy_parameters` to a Transform
**Context:**

`convert_to_numpy_parameters` is not a proper transform and can't be used within the existing transforms mechanisms. 

**Description of the Change:**

`@qml.transform` has been applied to `convert_to_numpy_parameters` and its output has been adapted accordingly. The code and tests that rely on the original function have also been updated accordingly.

**Benefits:**
- Can be used as a transform and composited with other transforms.
- Easier to track when applied.

[[sc-51708](https://app.shortcut.com/xanaduai/story/51708)]


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5693?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.67%. Comparing base [(`4ee09b8`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/4ee09b8896fd9b71bccf75634c27a81e29343c48?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`ebfb100`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5693?dropdown=coverage&src=pr&el=desc&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    #5693      +/-   ##
==========================================
- Coverage   99.68%   99.67%   -0.01%     
==========================================
  Files         414      414              
  Lines       38844    38558     -286     
==========================================
- Hits        38720    38433     -287     
- Misses        124      125       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5693?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 1a65146a5..1571d9276 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -86,6 +86,9 @@
 
 * Implemented kwargs (`check_interface`, `check_trainability`, `rtol` and `atol`) support in `qml.equal` for the operators `Pow`, `Adjoint`, `Exp`, and `SProd`.
   [(#5668)](https://github.com/PennyLaneAI/pennylane/issues/5668)
+  
+* ``qml.QutritDepolarizingChannel`` has been added, allowing for depolarizing noise to be simulated on the `default.qutrit.mixed` device.
+  [(#5502)](https://github.com/PennyLaneAI/pennylane/pull/5502)
 
 <h3>Breaking changes 💔</h3>
 
@@ -98,10 +101,9 @@
 * `qml.load` has been removed in favour of more specific functions, such as `qml.from_qiskit`, etc.
   [(#5654)](https://github.com/PennyLaneAI/pennylane/pull/5654)
 
-<h4>Community contributions 🥳</h4>
-
-* ``qml.QutritDepolarizingChannel`` has been added, allowing for depolarizing noise to be simulated on the `default.qutrit.mixed` device.
-  [(#5502)](https://github.com/PennyLaneAI/pennylane/pull/5502)
+* `qml.transforms.convert_to_numpy_parameters` is now a proper transform and its output signature has changed,
+  returning a list of `QuantumTape`s and a post-processing function instead of simply the transformed circuit.
+  [(#5693)](https://github.com/PennyLaneAI/pennylane/pull/5693)
 
 <h3>Deprecations 👋</h3>
 
@@ -140,6 +142,7 @@
 
 This release contains contributions from (in alphabetical order):
 
+Ahmed Darwish,
 Gabriel Bottrill,
 Isaac De Vlugt,
 Pietropaolo Frisoni,
diff --git a/pennylane/devices/default_clifford.py b/pennylane/devices/default_clifford.py
index 7a1d31035..9e92f4c95 100644
--- a/pennylane/devices/default_clifford.py
+++ b/pennylane/devices/default_clifford.py
@@ -507,7 +507,7 @@ class DefaultClifford(Device):
             return tuple(
                 self.simulate(c, seed=s, debugger=self._debugger) for c, s in zip(circuits, seeds)
             )
-        vanilla_circuits = [convert_to_numpy_parameters(c) for c in circuits]
+        vanilla_circuits = convert_to_numpy_parameters(circuits)[0]
         seeds = self._rng.integers(2**31 - 1, size=len(vanilla_circuits))
         _wrap_simulate = partial(self.simulate, debugger=None)
         with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index d7957424b..6a1eebd81 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -605,7 +605,7 @@ class DefaultQubit(Device):
                 for c, _key in zip(circuits, prng_keys)
             )
 
-        vanilla_circuits = [convert_to_numpy_parameters(c) for c in circuits]
+        vanilla_circuits = convert_to_numpy_parameters(circuits)[0]
         seeds = self._rng.integers(2**31 - 1, size=len(vanilla_circuits))
         simulate_kwargs = [{"rng": _rng, "prng_key": _key} for _rng, _key in zip(seeds, prng_keys)]
 
@@ -627,7 +627,7 @@ class DefaultQubit(Device):
         if max_workers is None:
             return tuple(adjoint_jacobian(circuit) for circuit in circuits)
 
-        vanilla_circuits = [convert_to_numpy_parameters(c) for c in circuits]
+        vanilla_circuits = convert_to_numpy_parameters(circuits)[0]
         with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
             exec_map = executor.map(adjoint_jacobian, vanilla_circuits)
             res = tuple(exec_map)
@@ -647,7 +647,7 @@ class DefaultQubit(Device):
         if max_workers is None:
             results = tuple(_adjoint_jac_wrapper(c, debugger=self._debugger) for c in circuits)
         else:
-            vanilla_circuits = [convert_to_numpy_parameters(c) for c in circuits]
+            vanilla_circuits = convert_to_numpy_parameters(circuits)[0]
 
             with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
                 results = tuple(
@@ -688,7 +688,7 @@ class DefaultQubit(Device):
         if max_workers is None:
             return tuple(adjoint_jvp(circuit, tans) for circuit, tans in zip(circuits, tangents))
 
-        vanilla_circuits = [convert_to_numpy_parameters(c) for c in circuits]
+        vanilla_circuits = convert_to_numpy_parameters(circuits)[0]
         with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
             res = tuple(executor.map(adjoint_jvp, vanilla_circuits, tangents))
 
@@ -711,7 +711,7 @@ class DefaultQubit(Device):
                 for c, t in zip(circuits, tangents)
             )
         else:
-            vanilla_circuits = [convert_to_numpy_parameters(c) for c in circuits]
+            vanilla_circuits = convert_to_numpy_parameters(circuits)[0]
 
             with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
                 results = tuple(
@@ -801,7 +801,7 @@ class DefaultQubit(Device):
                 for circuit, cots in zip(circuits, cotangents)
             )
 
-        vanilla_circuits = [convert_to_numpy_parameters(c) for c in circuits]
+        vanilla_circuits = convert_to_numpy_parameters(circuits)[0]
         with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
             res = tuple(executor.map(adjoint_vjp, vanilla_circuits, cotangents))
 
@@ -824,7 +824,7 @@ class DefaultQubit(Device):
                 for c, t in zip(circuits, cotangents)
             )
         else:
-            vanilla_circuits = [convert_to_numpy_parameters(c) for c in circuits]
+            vanilla_circuits = convert_to_numpy_parameters(circuits)[0]
 
             with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
                 results = tuple(
diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py
index 8f5e660a0..ab9b1ba26 100644
--- a/pennylane/tape/qscript.py
+++ b/pennylane/tape/qscript.py
@@ -1060,7 +1060,8 @@ class QuantumScript:
         qasm_str += f"creg c[{len(wires)}];\n"
 
         # get the user applied circuit operations without interface information
-        operations = qml.transforms.convert_to_numpy_parameters(self).operations
+        [transformed_tape], _ = qml.transforms.convert_to_numpy_parameters(self)
+        operations = transformed_tape.operations
 
         if rotations:
             # if requested, append diagonalizing gates corresponding
diff --git a/pennylane/transforms/convert_to_numpy_parameters.py b/pennylane/transforms/convert_to_numpy_parameters.py
index ed4a910b2..be483431d 100644
--- a/pennylane/transforms/convert_to_numpy_parameters.py
+++ b/pennylane/transforms/convert_to_numpy_parameters.py
@@ -15,9 +15,12 @@
 This file contains preprocessings steps that may be called internally
 during execution.
 """
+from typing import Callable, Sequence, Tuple
+
 import pennylane as qml
 from pennylane import math
-from pennylane.tape import QuantumScript
+from pennylane.tape import QuantumTape
+from pennylane.transforms import transform
 
 
 # pylint: disable=no-member
@@ -44,14 +47,15 @@ def _convert_measurement_to_numpy_data(
 
 
 # pylint: disable=protected-access
-def convert_to_numpy_parameters(circuit: QuantumScript) -> QuantumScript:
+@transform
+def convert_to_numpy_parameters(tape: QuantumTape) -> Tuple[Sequence[QuantumTape], Callable]:
     """Transforms a circuit to one with purely numpy parameters.
 
     Args:
         circuit (QuantumScript): a circuit with parameters of any interface
 
     Returns:
-        QuantumScript: A circuit with purely numpy parameters
+        tuple[List[QuantumTape], function]: The transformed circuits along with a dummy post-processing function.
 
     .. seealso::
 
@@ -60,8 +64,8 @@ def convert_to_numpy_parameters(circuit: QuantumScript) -> QuantumScript:
 
     >>> ops = [qml.S(0), qml.RX(torch.tensor(0.1234), 0)]
     >>> measurements = [qml.state(), qml.expval(qml.Hermitian(torch.eye(2), 0))]
-    >>> circuit = qml.tape.QuantumScript(ops, measurements )
-    >>> new_circuit = convert_to_numpy_parameters(circuit)
+    >>> circuit = qml.tape.QuantumScript(ops, measurements)
+    >>> [new_circuit], _ = convert_to_numpy_parameters(circuit)
     >>> new_circuit.circuit
     [S(wires=[0]),
     RX(0.1234000027179718, wires=[0]),
@@ -81,9 +85,16 @@ def convert_to_numpy_parameters(circuit: QuantumScript) -> QuantumScript:
     False
 
     """
-    new_ops = (_convert_op_to_numpy_data(op) for op in circuit.operations)
-    new_measurements = (_convert_measurement_to_numpy_data(m) for m in circuit.measurements)
-    new_circuit = circuit.__class__(
-        new_ops, new_measurements, shots=circuit.shots, trainable_params=circuit.trainable_params
+    new_ops = (_convert_op_to_numpy_data(op) for op in tape.operations)
+    new_measurements = (_convert_measurement_to_numpy_data(m) for m in tape.measurements)
+    new_circuit = tape.__class__(
+        new_ops, new_measurements, shots=tape.shots, trainable_params=tape.trainable_params
     )
-    return new_circuit
+
+    def null_postprocessing(results):
+        """A postprocesing function returned by a transform that only converts the batch of results
+        into a result for a single ``QuantumTape``.
+        """
+        return results[0]
+
+    return [new_circuit], null_postprocessing
diff --git a/pennylane/workflow/execution.py b/pennylane/workflow/execution.py
index 1d2e1f3e2..4348e5f78 100644
--- a/pennylane/workflow/execution.py
+++ b/pennylane/workflow/execution.py
@@ -259,9 +259,9 @@ def _make_inner_execute(
     for the 1st order derivatives.
 
     Steps in between the ml framework execution and the device are:
-    - caching
-    - conversion to numpy
     - device expansion (old device)
+    - conversion to numpy
+    - caching
 
     For higher order derivatives, the "inner execute" will be another ml framework execute.
     """
@@ -282,14 +282,15 @@ def _make_inner_execute(
         """
         transform_program = qml.transforms.core.TransformProgram()
 
+        if numpy_only:
+            transform_program.add_transform(qml.transforms.convert_to_numpy_parameters)
+
         if cache is not None:
             transform_program.add_transform(_cache_transform, cache=cache)
 
-        # TODO: Apply expand_fn() and convert_to_numpy_parameters() as transforms.
+        # TODO: Apply expand_fn() as transform.
         if expand_fn:
             tapes = tuple(expand_fn(t) for t in tapes)
-        if numpy_only:
-            tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
 
         transformed_tapes, transform_post_processing = transform_program(tapes)
 
@@ -652,9 +653,8 @@ def execute(
                     device: The device to execute on
                     config: the ExecutionConfig that specifies how to perform the simulations.
                 """
-                numpy_tapes = tuple(
-                    qml.transforms.convert_to_numpy_parameters(t) for t in internal_tapes
-                )
+                numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(internal_tapes)
+
                 return device.execute_and_compute_derivatives(numpy_tapes, config)
 
             gradient_fn = None
@@ -668,9 +668,7 @@ def execute(
                     device: the device to execute on
                     config: the ExecutionConfig that specifies how to perform the simulations.
                 """
-                numpy_tapes = tuple(
-                    qml.transforms.convert_to_numpy_parameters(t) for t in internal_tapes
-                )
+                numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(internal_tapes)
                 return (device.execute(numpy_tapes, config), tuple())
 
             def gradient_fn(internal_tapes):
@@ -680,9 +678,7 @@ def execute(
                     device: the device to execute on
                     config: the ExecutionConfig that specifies how to take the derivative.
                 """
-                numpy_tapes = tuple(
-                    qml.transforms.convert_to_numpy_parameters(t) for t in internal_tapes
-                )
+                numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(internal_tapes)
                 return device.compute_derivatives(numpy_tapes, config)
 
     elif gradient_fn == "device":
@@ -708,9 +704,7 @@ def execute(
             # replace the forward execution function to return
             # both results and gradients
             def device_execute_and_gradients(internal_tapes, **gradient_kwargs):
-                numpy_tapes = tuple(
-                    qml.transforms.convert_to_numpy_parameters(t) for t in internal_tapes
-                )
+                numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(internal_tapes)
                 return set_shots(device, override_shots)(device.execute_and_gradients)(
                     numpy_tapes, **gradient_kwargs
                 )
@@ -732,9 +726,7 @@ def execute(
             cached_gradient_fn = _apply_cache_transform(fn=gradient_fn_with_shots, cache=cache)
 
             def device_gradient_fn(inner_tapes, **gradient_kwargs):
-                numpy_tapes = tuple(
-                    qml.transforms.convert_to_numpy_parameters(t) for t in inner_tapes
-                )
+                numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(inner_tapes)
                 return cached_gradient_fn(numpy_tapes, **gradient_kwargs)
 
             gradient_fn = device_gradient_fn
diff --git a/pennylane/workflow/interfaces/jax.py b/pennylane/workflow/interfaces/jax.py
index 5b3d0b869..141a4f99d 100644
--- a/pennylane/workflow/interfaces/jax.py
+++ b/pennylane/workflow/interfaces/jax.py
@@ -167,7 +167,7 @@ class _NonPytreeWrapper:
 def _set_copy_and_unwrap_tape(t, a, unwrap=True):
     """Copy a given tape with operations and set parameters"""
     tc = t.bind_new_parameters(a, t.trainable_params)
-    return convert_to_numpy_parameters(tc) if unwrap else tc
+    return convert_to_numpy_parameters(tc)[0][0] if unwrap else tc
 
 
 def set_parameters_on_copy_and_unwrap(tapes, params, unwrap=True):
diff --git a/pennylane/workflow/interfaces/tensorflow.py b/pennylane/workflow/interfaces/tensorflow.py
index ef571011d..514f14b19 100644
--- a/pennylane/workflow/interfaces/tensorflow.py
+++ b/pennylane/workflow/interfaces/tensorflow.py
@@ -221,7 +221,7 @@ def tf_execute(tapes, execute_fn, jpc, device=None, differentiable=False):
             [i.numpy() if isinstance(i, (tf.Variable, tf.Tensor)) else i for i in params]
         )
 
-    numpy_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
+    numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
 
     tapes = tuple(tapes)
 
diff --git a/pennylane/workflow/jacobian_products.py b/pennylane/workflow/jacobian_products.py
index 83343d137..e8d9832e6 100644
--- a/pennylane/workflow/jacobian_products.py
+++ b/pennylane/workflow/jacobian_products.py
@@ -426,7 +426,7 @@ class DeviceDerivatives(JacobianProductCalculator):
 
         Dispatches between the two different device interfaces.
         """
-        numpy_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
+        numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
         if self._uses_new_device:
             return self._device.execute_and_compute_derivatives(numpy_tapes, self._execution_config)
         return self._device.execute_and_gradients(numpy_tapes, **self._gradient_kwargs)
@@ -437,7 +437,7 @@ class DeviceDerivatives(JacobianProductCalculator):
 
         Dispatches between the two different device interfaces.
         """
-        numpy_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
+        numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
         if self._uses_new_device:
             return self._device.execute(numpy_tapes, self._execution_config)
         return self._device.batch_execute(numpy_tapes)
@@ -448,7 +448,7 @@ class DeviceDerivatives(JacobianProductCalculator):
 
         Dispatches between the two different device interfaces.
         """
-        numpy_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
+        numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
         if self._uses_new_device:
             return self._device.compute_derivatives(numpy_tapes, self._execution_config)
         return self._device.gradients(numpy_tapes, **self._gradient_kwargs)
@@ -669,14 +669,14 @@ class DeviceJacobianProducts(JacobianProductCalculator):
     ) -> Tuple[ResultBatch, Tuple]:
         if logger.isEnabledFor(logging.DEBUG):  # pragma: no cover
             logger.debug("execute_and_compute_jvp called with (%s, %s)", tapes, tangents)
-        numpy_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
+        numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
         tangents = qml.math.unwrap(tangents)
         return self._device.execute_and_compute_jvp(numpy_tapes, tangents, self._execution_config)
 
     def compute_vjp(self, tapes: Batch, dy: Tuple[Tuple[TensorLike]]) -> Tuple:
         if logger.isEnabledFor(logging.DEBUG):  # pragma: no cover
             logger.debug("compute_vjp called with (%s, %s)", tapes, dy)
-        numpy_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
+        numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
         dy = qml.math.unwrap(dy)
         vjps = self._device.compute_vjp(numpy_tapes, dy, self._execution_config)
         res = []
@@ -690,13 +690,13 @@ class DeviceJacobianProducts(JacobianProductCalculator):
     def compute_jacobian(self, tapes: Batch):
         if logger.isEnabledFor(logging.DEBUG):  # pragma: no cover
             logger.debug("compute_jacobian called with %s", tapes)
-        numpy_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
+        numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
         return self._device.compute_derivatives(numpy_tapes, self._execution_config)
 
     def execute_and_compute_jacobian(self, tapes: Batch) -> Tuple:
         if logger.isEnabledFor(logging.DEBUG):  # pragma: no cover
             logger.debug("execute_and_compute_jacobian called with %s", tapes)
-        numpy_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes)
+        numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
         return self._device.execute_and_compute_derivatives(numpy_tapes, self._execution_config)
 
 
@@ -739,8 +739,8 @@ class LightningVJPs(DeviceDerivatives):
         ):
             raise NotImplementedError("Lightning device VJPs only support expectation values.")
         results = []
-        for dyi, tape in zip(dy, tapes):
-            numpy_tape = qml.transforms.convert_to_numpy_parameters(tape)
+        numpy_tapes, _ = qml.transforms.convert_to_numpy_parameters(tapes)
+        for dyi, tape in zip(dy, numpy_tapes):
             if len(tape.measurements) == 1:
                 dyi = (dyi,)
             dyi = np.array(qml.math.unwrap(dyi))
@@ -748,10 +748,8 @@ class LightningVJPs(DeviceDerivatives):
                 raise NotImplementedError(
                     "Lightning device VJPs are not supported with jax jacobians."
                 )
-            vjp_f = self._device.vjp(
-                numpy_tape.measurements, dyi, **self._processed_gradient_kwargs
-            )
-            out = vjp_f(numpy_tape)
+            vjp_f = self._device.vjp(tape.measurements, dyi, **self._processed_gradient_kwargs)
+            out = vjp_f(tape)
             if len(tape.trainable_params) == 1:
                 out = (out,)
             results.append(out)


Test patch:
diff --git a/tests/interfaces/test_tensorflow.py b/tests/interfaces/test_tensorflow.py
index 1e690e08c..057f93d00 100644
--- a/tests/interfaces/test_tensorflow.py
+++ b/tests/interfaces/test_tensorflow.py
@@ -179,7 +179,7 @@ class TestCaching:
         cache = spy.call_args.kwargs["cache"]
         assert cache is custom_cache
 
-        unwrapped_tape = qml.transforms.convert_to_numpy_parameters(tape)
+        unwrapped_tape = qml.transforms.convert_to_numpy_parameters(tape)[0][0]
         h = unwrapped_tape.hash
 
         assert h in cache
diff --git a/tests/transforms/test_convert_to_numpy_parameters.py b/tests/transforms/test_convert_to_numpy_parameters.py
index b7094960f..62d701376 100644
--- a/tests/transforms/test_convert_to_numpy_parameters.py
+++ b/tests/transforms/test_convert_to_numpy_parameters.py
@@ -52,12 +52,17 @@ def test_convert_arrays_to_numpy(framework, shots):
     m = [qml.state(), qml.expval(qml.Hermitian(M, 0))]
 
     qs = qml.tape.QuantumScript(ops, m, shots=shots)
-    new_qs = convert_to_numpy_parameters(qs)
+    new_qs, fn = convert_to_numpy_parameters(qs)
 
+    assert len(new_qs) == 1
+    assert isinstance(new_qs[0], qml.tape.QuantumScript)
+
+    new_qs = new_qs[0]
     # check ops that should be unaltered
     assert new_qs[3] is qs[3]
     assert new_qs[4] is qs[4]
     assert new_qs.measurements[0] is qs.measurements[0]
+    assert fn([0.5]) == 0.5
 
     for ind in (0, 1, 2):
         assert qml.equal(new_qs[ind], qs[ind], check_interface=False, check_trainability=False)
@@ -75,8 +80,8 @@ def test_preserves_trainable_params():
     ops = [qml.RX(qml.numpy.array(2.0), 0), qml.RY(qml.numpy.array(3.0), 0)]
     qs = qml.tape.QuantumScript(ops)
     qs.trainable_params = {0}
-    output = convert_to_numpy_parameters(qs)
-    assert output.trainable_params == [0]
+    output, _ = convert_to_numpy_parameters(qs)
+    assert output[0].trainable_params == [0]
 
 
 @pytest.mark.autograd