Repository: PennyLaneAI/pennylane
Pull request #1700 (2021-09-29 10:01:20)

Add `is_independent` util
**Context:**
In order to validate `classical_jacobian`s to be constant, asserting linearity of classical preprocessing between QNode arguments and gate arguments, we require a testing function for a callable to be independent of its arguments.

**Description of the Change:**
New function `qml.math.is_independent` that performs the test both, numerically and analytically. For PyTorch, only the numerical test is available, which is marked with a `UserWarning`.

**Benefits:**
Preparatory addition of a utility function for the new `spectrum` implementation (see PR #1681 )

**Possible Drawbacks:**
If its scope is misunderstood, this function might yield confusing results. However, the docstring makes clear that this is the case.



codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1700?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1700](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1700?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (bbb68b1) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/5d55cf8fe5e68fba42e6c52b42acde7fce3b2cf4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (5d55cf8) will **decrease** coverage by `0.00%`.
> The diff coverage is `98.59%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1700      +/-   ##
==========================================
- Coverage   99.20%   99.20%   -0.01%     
==========================================
  Files         201      202       +1     
  Lines       15122    15193      +71     
==========================================
+ Hits        15002    15072      +70     
- Misses        120      121       +1     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1700?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/math/utils.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1700/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvdXRpbHMucHk=) | `100.00% <ø> (ø)` | |
| [pennylane/math/\_is\_independent.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1700/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvX2lzX2luZGVwZW5kZW50LnB5) | `98.57% <98.57%> (ø)` | |
| [pennylane/math/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1700/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |

------

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

dwierichs: @glassnotes Looking forward to your review :) 
dwierichs: Thank you for the quick review! An analytic test with torch would indeed be nice. In the worst case, changing the interface  to validate this for a contrived example is always an option... 

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index f0dca26fe..07b0cae1d 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -4,6 +4,25 @@
 
 <h3>New features since last release</h3>
 
+* There is a new utility function `qml.math.is_independent` that checks whether
+  a callable is independent of its arguments.
+  [(#1700)](https://github.com/PennyLaneAI/pennylane/pull/1700)
+
+  **Warning**
+
+  This function is experimental and might behave differently than expected.
+  Also, it might be subject to change.
+
+  **Disclaimer**
+
+  Note that the test relies on both numerical and analytical checks, except
+  when using the PyTorch interface which only performs a numerical check.
+  It is known that there are edge cases on which this test will yield wrong
+  results, in particular non-smooth functions may be problematic.
+  For details, please refer to the 
+  [is_indpendent docstring](https://pennylane.readthedocs.io/en/latest/code/api/pennylane.math.is_independent.html).
+
+
 * Support for differentiable execution of batches of circuits has been
   extended to the JAX interface for scalar functions, via the beta
   `pennylane.interfaces.batch` module.
diff --git a/pennylane/math/__init__.py b/pennylane/math/__init__.py
index 969273142..c4b9eb0e4 100644
--- a/pennylane/math/__init__.py
+++ b/pennylane/math/__init__.py
@@ -59,6 +59,7 @@ from .utils import (
     requires_grad,
 )
 
+from .is_independent import is_independent
 
 sum = ar.numpy.sum
 toarray = ar.numpy.to_numpy
@@ -90,4 +91,5 @@ __all__ = [
     "unwrap",
     "frobenius_inner_product",
     "get_trainable_indices",
+    "is_independent",
 ]
diff --git a/pennylane/math/is_independent.py b/pennylane/math/is_independent.py
new file mode 100644
index 000000000..ee2d6321f
--- /dev/null
+++ b/pennylane/math/is_independent.py
@@ -0,0 +1,377 @@
+# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+This file contains the is_independent function that checks if
+a function is independent of its arguments for the interfaces
+
+* Autograd
+* JAX
+* TensorFlow
+* PyTorch
+"""
+import warnings
+
+import numpy as np
+
+from autograd.tracer import isbox, new_box, trace_stack
+from autograd.core import VJPNode
+
+
+def _autograd_is_indep_analytic(func, *args, **kwargs):
+    """Test analytically whether a function is independent of its arguments
+    using Autograd.
+
+    Args:
+        func (callable): Function to test for independence
+        args (tuple): Arguments for the function with respect to which
+            to test for independence
+        kwargs (dict): Keyword arguments for the function at which
+            (but not with respect to which) to test for independence
+
+    Returns:
+        bool: Whether the function seems to not depend on it ``args``
+        analytically. That is, an output of ``True`` means that the
+        ``args`` do *not* feed into the output.
+
+    In Autograd, we test this by sending a ``Box`` through the function and
+    testing whether the output is again a ``Box`` and on the same trace as
+    the input ``Box``. This means that we can trace actual *independence*
+    of the output from the input, not only whether the passed function is
+    constant.
+    The code is adapted from
+    `autograd.tracer.py::trace
+    <https://github.com/HIPS/autograd/blob/master/autograd/tracer.py#L7>`__.
+    """
+    # pylint: disable=protected-access
+    node = VJPNode.new_root()
+    with trace_stack.new_trace() as t:
+        start_box = new_box(args, t, node)
+        end_box = func(*start_box, **kwargs)
+
+    if type(end_box) in [tuple, list]:
+        if any(isbox(_end) and _end._trace == start_box._trace for _end in end_box):
+            return False
+    elif isinstance(end_box, np.ndarray):
+        if end_box.ndim == 0:
+            end_box = [end_box.item()]
+        if any(isbox(_end) and _end._trace == start_box._trace for _end in end_box):
+            return False
+    else:
+        if isbox(end_box) and end_box._trace == start_box._trace:
+            return False
+    return True
+
+
+def _jax_is_indep_analytic(func, *args, **kwargs):
+    """Test analytically whether a function is independent of its arguments
+    using JAX.
+
+    Args:
+        func (callable): Function to test for independence
+        args (tuple): Arguments for the function with respect to which
+            to test for independence
+        kwargs (dict): Keyword arguments for the function at which
+            (but not with respect to which) to test for independence
+
+    Returns:
+        bool: Whether the function seems to not depend on it ``args``
+        analytically. That is, an output of ``True`` means that the
+        ``args`` do *not* feed into the output.
+
+    In JAX, we test this by constructing the VJP of the passed function
+    and inspecting its signature.
+    The first argument of the output of ``jax.vjp`` is a ``Partial``.
+    If *any* processing happens to any input, the arguments of that
+    ``Partial`` are unequal to ``((),)`.
+    Functions that depend on the input in a trivial manner, i.e., without
+    processing it, will go undetected by this. Therefore we also
+    test the arguments of the *function* of the above ``Partial``.
+    The first of these arguments is a list of tuples and if the
+    first entry of the first tuple is not ``None``, the input arguments
+    are detected to actually feed into the output.
+
+    .. warning::
+
+        This is an experimental function and unknown edge
+        cases may exist to this two-stage test.
+    """
+    import jax  # pylint: disable=import-outside-toplevel
+
+    mapped_func = lambda *_args: func(*_args, **kwargs)  # pylint: disable=unnecessary-lambda
+    _vjp = jax.vjp(mapped_func, *args)[1]
+    if _vjp.args[0].args != ((),):
+        return False
+    if _vjp.args[0].func.args[0][0][0] is not None:
+        return False
+
+    return True
+
+
+def _tf_is_indep_analytic(func, *args, **kwargs):
+    """Test analytically whether a function is independent of its arguments
+    using TensorFlow.
+
+    Args:
+        func (callable): Function to test for independence
+        args (tuple): Arguments for the function with respect to which
+            to test for independence
+        kwargs (dict): Keyword arguments for the function at which
+            (but not with respect to which) to test for independence
+
+    Returns:
+        bool: Whether the function seems to not depend on it ``args``
+        analytically. That is, an output of ``True`` means that the
+        ``args`` do *not* feed into the output.
+
+    In TensorFlow, we test this by computing the Jacobian of the output(s)
+    with respect to the arguments. If the Jacobian is ``None``, the output(s)
+    is/are independent.
+
+    .. note::
+
+        Of all interfaces, this is currently the most robust for the
+        ``is_independent`` functionality.
+    """
+    import tensorflow as tf  # pylint: disable=import-outside-toplevel
+
+    with tf.GradientTape(persistent=True) as tape:
+        out = func(*args, **kwargs)
+
+    if isinstance(out, tuple):
+        jac = [tape.jacobian(_out, args) for _out in out]
+        return all(all(__jac is None for __jac in _jac) for _jac in jac)
+
+    jac = tape.jacobian(out, args)
+    return all(_jac is None for _jac in jac)
+
+
+def _get_random_args(args, interface, num, seed, bounds):
+    r"""Generate random arguments of a given structure.
+
+    Args:
+        args (tuple): Original input arguments
+        interface (str): Interface of the QNode into which the arguments will be fed
+        num (int): Number of random argument sets to generate
+        seed (int): Seed for random generation
+        bounds (tuple[int]): Range within which to sample the random parameters.
+
+    Returns:
+        list[tuple]: List of length ``num`` with each entry being a random instance
+        of arguments like ``args``.
+
+    This function generates ``num`` many tuples of random arguments in the given range
+    that have the same shapes as ``args``.
+    """
+    width = bounds[1] - bounds[0]
+    if interface == "tf":
+        import tensorflow as tf  # pylint: disable=import-outside-toplevel
+
+        tf.random.set_seed(seed)
+        rnd_args = []
+        for _ in range(num):
+            _args = (tf.random.uniform(tf.shape(_arg)) * width + bounds[0] for _arg in args)
+            _args = tuple(
+                tf.Variable(_arg) if isinstance(arg, tf.Variable) else _arg
+                for _arg, arg in zip(_args, args)
+            )
+            rnd_args.append(_args)
+    elif interface == "torch":
+        import torch  # pylint: disable=import-outside-toplevel
+
+        torch.random.manual_seed(seed)
+        rnd_args = [
+            tuple(torch.rand(np.shape(arg)) * width + bounds[0] for arg in args) for _ in range(num)
+        ]
+    else:
+        np.random.seed(seed)
+        rnd_args = [
+            tuple(np.random.random(np.shape(arg)) * width + bounds[0] for arg in args)
+            for _ in range(num)
+        ]
+
+    return rnd_args
+
+
+def _is_indep_numerical(func, interface, args, kwargs, num_pos, seed, atol, rtol, bounds):
+    """Test whether a function returns the same output at random positions.
+
+    Args:
+        func (callable): Function to be tested
+        interface (str): Interface used by ``func``
+        args (tuple): Positional arguments with respect to which to test
+        kwargs (dict): Keyword arguments for ``func`` at which to test;
+            the ``kwargs`` are kept fixed in this test.
+        num_pos (int): Number of random positions to test
+        seed (int): Seed for random number generator
+        atol (float): Absolute tolerance for comparing the outputs
+        rtol (float): Relative tolerance for comparing the outputs
+        bounds (tuple[int, int]): Limits of the range from which to sample
+
+    Returns:
+        bool: Whether ``func`` returns the same output at the randomly
+        chosen points.
+    """
+
+    # pylint:disable=too-many-arguments
+
+    rnd_args = _get_random_args(args, interface, num_pos, seed, bounds)
+    original_output = func(*args, **kwargs)
+    is_tuple_valued = isinstance(original_output, tuple)
+    for _rnd_args in rnd_args:
+        new_output = func(*_rnd_args, **kwargs)
+        if is_tuple_valued:
+            if not all(
+                np.allclose(new, orig, atol=atol, rtol=rtol)
+                for new, orig in zip(new_output, original_output)
+            ):
+                return False
+        else:
+            if not np.allclose(new_output, original_output, atol=atol, rtol=rtol):
+                return False
+
+    return True
+
+
+def is_independent(
+    func,
+    interface,
+    args,
+    kwargs=None,
+    num_pos=5,
+    seed=9123,
+    atol=1e-6,
+    rtol=0,
+    bounds=(-np.pi, np.pi),
+):
+    """Test whether a function is independent of its input arguments,
+    both numerically and analytically.
+
+    Args:
+        func (callable): Function to be tested
+        interface (str): Autodiff framework used by ``func``. Must correspond to one
+            of the supported PennyLane interface strings, such as ``"autograd"``,
+            ``"tf"``, ``"torch"``, ``"jax"``.
+        args (tuple): Positional arguments with respect to which to test
+        kwargs (dict): Keyword arguments for ``func`` at which to test;
+            the keyword arguments are kept fixed in this test.
+        num_pos (int): Number of random positions to test
+        seed (int): Seed for the random number generator
+        atol (float): Absolute tolerance for comparing the outputs
+        rtol (float): Relative tolerance for comparing the outputs
+        bounds (tuple[float]): 2-tuple containing limits of the range from which to sample
+
+    Returns:
+        bool: Whether ``func`` returns the same output at randomly
+        chosen points and is numerically independent of its arguments.
+
+    .. warning::
+
+        This function is experimental.
+        As such, it might yield wrong results and might behave
+        slightly differently in distinct autodifferentiation frameworks
+        for some edge cases.
+        For example, a currently known edge case are piecewise
+        functions that use classical control and simultaneously
+        return (almost) constant output, such as
+
+        .. code-block:: python
+
+            def func(x):
+                if abs(x) <1e-5:
+                    return x
+                else:
+                    return 0. * x
+
+    The analytic and numeric tests used are as follows.
+
+    - The analytic test performed depends on the provided ``interface``,
+       both in its method and its degree of reliability.
+
+    - For the numeric test, the function is evaluated at a series of random positions,
+      and the outputs numerically compared to verify that the output
+      is constant.
+
+    .. warning ::
+
+        Currently, no analytic test is available for the PyTorch interface.
+        When using PyTorch, a warning will be raised and only the
+        numeric test is performed.
+
+    .. note ::
+
+        Due to the structure of ``is_independent``, it is possible that it
+        errs on the side of reporting a dependent function to be independent
+        (a false positive). However, reporting an independent function to be
+        dependent (a false negative) is *highly* unlikely.
+
+    **Example**
+
+    Consider the (linear) function
+
+    .. code-block:: python
+
+        def lin(x, weights=None):
+            return np.dot(x, weights)
+
+    This function clearly depends on ``x``. We may check for this via
+
+    .. code-block:: pycon
+
+        >>> x = np.array([0.2, 9.1, -3.2])
+        >>> weights = np.array([1.1, -0.7, 1.8])
+        >>> qml.math.is_independent(lin, "autograd", (x,), {"weights": weights})
+        False
+
+    However, the Jacobian will not depend on ``x`` because ``lin`` is a
+    linear function:
+
+    .. code-block:: pycon
+
+        >>> jac = qml.jacobian(lin)
+        >>> qml.math.is_independent(jac, "autograd", (x,), {"weights": weights})
+        True
+
+    Note that a function ``f = lambda x: 0.0 * x`` will be counted as *dependent* on ``x``
+    because it does depend on ``x`` *functionally*, even if the value is constant for all ``x``.
+    This means that ``is_independent`` is a stronger test than simply verifying functions
+    have constant output.
+    """
+
+    # pylint:disable=too-many-arguments
+
+    if not interface in {"autograd", "jax", "tf", "torch", "tensorflow"}:
+        raise ValueError(f"Unknown interface: {interface}")
+
+    kwargs = kwargs or {}
+
+    if interface == "autograd":
+        if not _autograd_is_indep_analytic(func, *args, **kwargs):
+            return False
+
+    if interface == "jax":
+        if not _jax_is_indep_analytic(func, *args, **kwargs):
+            return False
+
+    if interface in ("tf", "tensorflow"):
+        if not _tf_is_indep_analytic(func, *args, **kwargs):
+            return False
+
+    if interface == "torch":
+        warnings.warn(
+            "The function is_independent is only available numerically for the PyTorch interface."
+            " Make sure that sampling positions and evaluating the function at these positions"
+            " is a sufficient test, or change the interface."
+        )
+
+    return _is_indep_numerical(func, interface, args, kwargs, num_pos, seed, atol, rtol, bounds)
diff --git a/pennylane/math/utils.py b/pennylane/math/utils.py
index 585c930d3..837115fd5 100644
--- a/pennylane/math/utils.py
+++ b/pennylane/math/utils.py
@@ -260,7 +260,9 @@ def requires_grad(tensor, interface=None):
         try:
             from tensorflow.python.eager.tape import should_record_backprop
         except ImportError:  # pragma: no cover
-            from tensorflow.python.eager.tape import should_record as should_record_backprop
+            from tensorflow.python.eager.tape import (
+                should_record as should_record_backprop,
+            )
 
         return should_record_backprop([tf.convert_to_tensor(tensor)])
 


Test patch:
diff --git a/tests/math/test_is_independent.py b/tests/math/test_is_independent.py
new file mode 100644
index 000000000..647ccd205
--- /dev/null
+++ b/tests/math/test_is_independent.py
@@ -0,0 +1,548 @@
+# Copyright 2018-2020 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 :func:`pennylane.math.is_independent` function.
+"""
+import pytest
+
+import numpy as np
+
+import pennylane as qml
+from pennylane.math import is_independent
+from pennylane.math.is_independent import _get_random_args
+
+try:
+    import jax
+
+    have_jax = True
+except ImportError:
+    have_jax = False
+try:
+    import torch
+
+    have_torch = True
+except ImportError:
+    have_torch = False
+try:
+    import tensorflow as tf
+
+    have_tf = True
+except ImportError:
+    have_tf = False
+
+
+dependent_lambdas = [
+    lambda x: x,
+    lambda x: (x, x),
+    lambda x: [x] * 10,
+    lambda x: (2.0 * x, x),
+    lambda x: 0.0 * x,
+    lambda x, y: (0.0 * x, 0.0 * y),
+    lambda x: x if x > 0 else 0.0,  # RELU for x>0 is okay numerically
+    lambda x: x if x > 0 else 0.0,  # RELU for x<0 is okay numerically
+    lambda x: 1.0 if abs(x) < 1e-5 else 0.0,  # delta for x=0 is okay numerically
+    lambda x: x if abs(x) < 1e-5 else 0.0,  # x*delta for x=0 is okay
+    lambda x: 1.0 if x > 0 else 0.0,  # Heaviside is okay numerically
+    lambda x: 1.0 if x > 0 else 0.0,  # Heaviside is okay numerically
+    lambda x: qml.math.log(1 + qml.math.exp(1000.0 * x)) / 1000.0,  # Softplus is okay
+    lambda x: qml.math.log(1 + qml.math.exp(1000.0 * x)) / 1000.0,  # Softplus is okay
+]
+
+args_dependent_lambdas = [
+    (np.array(1.2),),
+    (2.19,),
+    (2.19,),
+    (1.0,),
+    (np.ones((2, 3)),),
+    (np.array([2.0, 5.0]), 1.2),
+    (1.6,),
+    (-2.0,),
+    (0.0,),
+    (0.0,),
+    (-2.0,),
+    (2.0,),
+    (-0.2,),
+    (0.9,),
+]
+
+lambdas_expect_torch_fail = [
+    False,
+    False,
+    False,
+    False,
+    True,
+    True,
+    False,
+    False,
+    False,
+    True,
+    False,
+    False,
+    False,
+    False,
+]
+
+overlooked_lambdas = [
+    lambda x: 1.0 if abs(x) < 1e-5 else 0.0,  # delta for x!=0 is not okay
+    lambda x: 1.0 if abs(x) < 1e-5 else 0.0,  # delta for x!=0 is not okay
+]
+
+args_overlooked_lambdas = [
+    (2.0,),
+    (-2.0,),
+]
+
+
+class TestIsIndependentAutograd:
+    """Tests for is_independent, which tests a function to be
+    independent of its inputs, using Autograd."""
+
+    interface = "autograd"
+
+    @pytest.mark.parametrize("num", [0, 1, 2])
+    @pytest.mark.parametrize(
+        "args",
+        [
+            (0.2,),
+            (1.1, 3.2, 0.2),
+            (np.array([[0, 9.2], [-1.2, 3.2]]),),
+            (0.3, [1, 4, 2], np.array([0.3, 9.1])),
+        ],
+    )
+    @pytest.mark.parametrize("bounds", [(-1, 1), (0.1, 1.0211)])
+    def test_get_random_args(self, args, num, bounds):
+        """Tests the utility ``_get_random_args`` using a fixed seed."""
+        seed = 921
+        rnd_args = _get_random_args(args, self.interface, num, seed, bounds)
+        assert len(rnd_args) == num
+        np.random.seed(seed)
+        for _rnd_args in rnd_args:
+            expected = tuple(
+                np.random.random(np.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0]
+                for arg in args
+            )
+            assert all(np.allclose(_exp, _rnd) for _exp, _rnd in zip(expected, _rnd_args))
+
+    dev = qml.device("default.qubit", wires=1)
+
+    @qml.qnode(dev, interface=interface)
+    def const_circuit(x, y):
+        qml.RX(0.1, wires=0)
+        return qml.expval(qml.PauliZ(0))
+
+    constant_functions = [
+        const_circuit,
+        lambda x: np.arange(20).reshape((2, 5, 2)),
+        lambda x: (np.ones(3), -0.1),
+        qml.jacobian(lambda x, y: 4 * x - 2.1 * y, argnum=[0, 1]),
+    ]
+
+    args_constant = [
+        (0.1, np.array([-2.1, 0.1])),
+        (1.2,),
+        (np.ones((2, 3)),),
+        (np.ones((3, 8)) * 0.1, -0.2 * np.ones((3, 8))),
+    ]
+
+    @qml.qnode(dev, interface=interface)
+    def dependent_circuit(x, y, z):
+        qml.RX(0.1, wires=0)
+        qml.RY(y / 2, wires=0)
+        qml.RZ(qml.math.sin(z), wires=0)
+        return qml.expval(qml.PauliX(0))
+
+    dependent_functions = [
+        dependent_circuit,
+        np.array,
+        lambda x: np.array(x * 0.0),
+        lambda x: (1 + qml.math.tanh(1000 * x)) / 2,
+        *dependent_lambdas,
+    ]
+
+    args_dependent = [
+        (0.1, np.array([-2.1, 0.1]), -0.9),
+        (-4.1,),
+        (-4.1,),
+        (np.ones((3, 8)) * 1.1,),
+        *args_dependent_lambdas,
+    ]
+
+    @pytest.mark.parametrize("func, args", zip(constant_functions, args_constant))
+    def test_independent(self, func, args):
+        """Tests that an independent function is correctly detected as such."""
+        assert is_independent(func, self.interface, args)
+
+    @pytest.mark.parametrize("func, args", zip(dependent_functions, args_dependent))
+    def test_dependent(self, func, args):
+        """Tests that a dependent function is correctly detected as such."""
+        assert not is_independent(func, self.interface, args)
+
+    @pytest.mark.xfail
+    @pytest.mark.parametrize("func, args", zip(overlooked_lambdas, args_overlooked_lambdas))
+    def test_overlooked_dependence(self, func, args):
+        """Test that particular functions that are dependent on the input
+        are overlooked."""
+        assert not is_independent(func, self.interface, args)
+
+    def test_kwargs_are_considered(self):
+        """Tests that kwargs are taken into account when checking
+        independence of outputs."""
+        f = lambda x, kw=False: 0.1 * x if kw else 0.2
+        jac = qml.jacobian(f, argnum=0)
+        args = (0.2,)
+        assert is_independent(f, self.interface, args)
+        assert not is_independent(f, self.interface, args, {"kw": True})
+        assert is_independent(jac, self.interface, args, {"kw": True})
+
+
+if have_jax:
+
+    class TestIsIndependentJax:
+        """Tests for is_independent, which tests a function to be
+        independent of its inputs, using JAX."""
+
+        interface = "jax"
+
+        @pytest.mark.parametrize("num", [0, 1, 2])
+        @pytest.mark.parametrize(
+            "args",
+            [
+                (0.2,),
+                (1.1, 3.2, 0.2),
+                (np.array([[0, 9.2], [-1.2, 3.2]]),),
+                (0.3, [1, 4, 2], np.array([0.3, 9.1])),
+            ],
+        )
+        @pytest.mark.parametrize("bounds", [(-1, 1), (0.1, 1.0211)])
+        def test_get_random_args(self, args, num, bounds):
+            """Tests the utility ``_get_random_args`` using a fixed seed."""
+            seed = 921
+            rnd_args = _get_random_args(args, self.interface, num, seed, bounds)
+            assert len(rnd_args) == num
+            np.random.seed(seed)
+            for _rnd_args in rnd_args:
+                expected = tuple(
+                    np.random.random(np.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0]
+                    for arg in args
+                )
+                assert all(np.allclose(_exp, _rnd) for _exp, _rnd in zip(expected, _rnd_args))
+
+        dev = qml.device("default.qubit", wires=1)
+
+        @qml.qnode(dev, interface=interface)
+        def const_circuit(x, y):
+            qml.RX(0.1, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        constant_functions = [
+            const_circuit,
+            lambda x: np.arange(20).reshape((2, 5, 2)),
+            lambda x: (np.ones(3), -0.1),
+            jax.jacobian(lambda x, y: 4.0 * x - 2.1 * y, argnums=[0, 1]),
+        ]
+
+        args_constant = [
+            (0.1, np.array([-2.1, 0.1])),
+            (1.2,),
+            (np.ones((2, 3)),),
+            (jax.numpy.ones((3, 8)) * 0.1, -0.2 * jax.numpy.ones((3, 8))),
+        ]
+
+        @qml.qnode(dev, interface=interface)
+        def dependent_circuit(x, y, z):
+            qml.RX(0.1, wires=0)
+            qml.RY(y / 2, wires=0)
+            qml.RZ(qml.math.sin(z), wires=0)
+            return qml.expval(qml.PauliX(0))
+
+        dependent_functions = [
+            dependent_circuit,
+            jax.numpy.array,
+            lambda x: (1 + qml.math.tanh(1000 * x)) / 2,
+            *dependent_lambdas,
+        ]
+
+        args_dependent = [
+            (0.1, np.array([-2.1, 0.1]), -0.9),
+            (-4.1,),
+            (jax.numpy.ones((3, 8)) * 1.1,),
+            *args_dependent_lambdas,
+        ]
+
+        @pytest.mark.parametrize("func, args", zip(constant_functions, args_constant))
+        def test_independent(self, func, args):
+            """Tests that an independent function is correctly detected as such."""
+            assert is_independent(func, self.interface, args)
+
+        @pytest.mark.parametrize("func, args", zip(dependent_functions, args_dependent))
+        def test_dependent(self, func, args):
+            """Tests that a dependent function is correctly detected as such."""
+            assert not is_independent(func, self.interface, args)
+
+        @pytest.mark.xfail
+        @pytest.mark.parametrize("func, args", zip(overlooked_lambdas, args_overlooked_lambdas))
+        def test_overlooked_dependence(self, func, args):
+            """Test that particular functions that are dependent on the input
+            are overlooked."""
+            assert not is_independent(func, self.interface, args)
+
+        def test_kwargs_are_considered(self):
+            """Tests that kwargs are taken into account when checking
+            independence of outputs."""
+            f = lambda x, kw=False: 0.1 * x if kw else 0.2
+            jac = jax.jacobian(f, argnums=0)
+            args = (0.2,)
+            assert is_independent(f, self.interface, args)
+            assert not is_independent(f, self.interface, args, {"kw": True})
+            assert is_independent(jac, self.interface, args, {"kw": True})
+
+
+if have_tf:
+
+    class TestIsIndependentTensorflow:
+        """Tests for is_independent, which tests a function to be
+        independent of its inputs, using Tensorflow."""
+
+        interface = "tf"
+
+        @pytest.mark.parametrize("num", [0, 1, 2])
+        @pytest.mark.parametrize(
+            "args",
+            [
+                (tf.Variable(0.2),),
+                (tf.Variable(1.1), tf.constant(3.2), tf.Variable(0.2)),
+                (tf.Variable(np.array([[0, 9.2], [-1.2, 3.2]])),),
+                (tf.Variable(0.3), [1, 4, 2], tf.Variable(np.array([0.3, 9.1]))),
+            ],
+        )
+        @pytest.mark.parametrize("bounds", [(-1, 1), (0.1, 1.0211)])
+        def test_get_random_args(self, args, num, bounds):
+            """Tests the utility ``_get_random_args`` using a fixed seed."""
+            tf = pytest.importorskip("tensorflow")
+            seed = 921
+            rnd_args = _get_random_args(args, self.interface, num, seed, bounds)
+            assert len(rnd_args) == num
+            tf.random.set_seed(seed)
+            for _rnd_args in rnd_args:
+                expected = tuple(
+                    tf.random.uniform(tf.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0]
+                    for arg in args
+                )
+                expected = tuple(
+                    tf.Variable(_exp) if isinstance(_arg, tf.Variable) else _exp
+                    for _arg, _exp in zip(args, expected)
+                )
+                assert all(np.allclose(_exp, _rnd) for _exp, _rnd in zip(expected, _rnd_args))
+
+        dev = qml.device("default.qubit", wires=1)
+
+        @qml.qnode(dev, interface=interface)
+        def const_circuit(x, y):
+            qml.RX(0.1, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        constant_functions = [
+            const_circuit,
+            lambda x: np.arange(20).reshape((2, 5, 2)),
+            lambda x: (np.ones(3), np.array(-0.1)),
+        ]
+
+        args_constant = [
+            (0.1, np.array([-2.1, 0.1])),
+            (1.2,),
+            (np.ones((2, 3)),),
+        ]
+
+        @qml.qnode(dev, interface=interface)
+        def dependent_circuit(x, y, z):
+            qml.RX(0.1, wires=0)
+            qml.RY(y / 2, wires=0)
+            qml.RZ(qml.math.sin(z), wires=0)
+            return qml.expval(qml.PauliX(0))
+
+        dependent_functions = [
+            dependent_circuit,
+            lambda x: (1 + qml.math.tanh(1000 * x)) / 2,
+            *dependent_lambdas,
+        ]
+
+        args_dependent = [
+            (tf.Variable(0.1), np.array([-2.1, 0.1]), tf.Variable(-0.9)),
+            (
+                tf.Variable(
+                    np.ones((3, 8)) * 1.1,
+                ),
+            ),
+            *args_dependent_lambdas,
+        ]
+
+        @pytest.mark.parametrize("func, args", zip(constant_functions, args_constant))
+        def test_independent(self, func, args):
+            """Tests that an independent function is correctly detected as such."""
+            args = tuple([tf.Variable(_arg) for _arg in args])
+            assert is_independent(func, self.interface, args)
+
+        @pytest.mark.parametrize("func, args", zip(dependent_functions, args_dependent))
+        def test_dependent(self, func, args):
+            """Tests that a dependent function is correctly detected as such."""
+            args = tuple([tf.Variable(_arg) for _arg in args])
+            # Filter out functions with TF-incompatible output format
+            out = func(*args)
+            if not isinstance(out, tf.Tensor):
+                try:
+                    _func = lambda *args: tf.Variable(func(*args))
+                    assert not is_independent(_func, self.interface, args)
+                except:
+                    pytest.skip()
+            else:
+                assert not is_independent(func, self.interface, args)
+
+        @pytest.mark.xfail
+        @pytest.mark.parametrize("func, args", zip(overlooked_lambdas, args_overlooked_lambdas))
+        def test_overlooked_dependence(self, func, args):
+            """Test that particular functions that are dependent on the input
+            are overlooked."""
+            assert not is_independent(func, self.interface, args)
+
+        def test_kwargs_are_considered(self):
+            """Tests that kwargs are taken into account when checking
+            independence of outputs."""
+            f = lambda x, kw=False: 0.1 * x if kw else tf.constant(0.2)
+
+            def _jac(x, kw):
+                with tf.GradientTape() as tape:
+                    out = f(x, kw)
+                return tape.jacobian(out, x)
+
+            args = (tf.Variable(0.2),)
+            assert is_independent(f, self.interface, args)
+            assert not is_independent(f, self.interface, args, {"kw": True})
+            assert is_independent(_jac, self.interface, args, {"kw": True})
+
+
+if have_torch:
+
+    class TestIsIndependentTorch:
+        """Tests for is_independent, which tests a function to be
+        independent of its inputs, using PyTorch."""
+
+        interface = "torch"
+
+        @pytest.mark.parametrize("num", [0, 1, 2])
+        @pytest.mark.parametrize(
+            "args",
+            [
+                (torch.tensor(0.2),),
+                (1.1, 3.2, torch.tensor(0.2)),
+                (torch.tensor([[0, 9.2], [-1.2, 3.2]]),),
+                (0.3, torch.tensor([1, 4, 2]), torch.tensor([0.3, 9.1])),
+            ],
+        )
+        @pytest.mark.parametrize("bounds", [(-1, 1), (0.1, 1.0211)])
+        def test_get_random_args(self, args, num, bounds):
+            """Tests the utility ``_get_random_args`` using a fixed seed."""
+            torch = pytest.importorskip("torch")
+            seed = 921
+            rnd_args = _get_random_args(args, self.interface, num, seed, bounds)
+            assert len(rnd_args) == num
+            torch.random.manual_seed(seed)
+            for _rnd_args in rnd_args:
+                expected = tuple(
+                    torch.rand(np.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0] for arg in args
+                )
+                assert all(np.allclose(_exp, _rnd) for _exp, _rnd in zip(expected, _rnd_args))
+
+        dev = qml.device("default.qubit", wires=1)
+
+        @qml.qnode(dev, interface=interface)
+        def const_circuit(x, y):
+            qml.RX(0.1, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        constant_functions = [
+            const_circuit,
+            lambda x: np.arange(20).reshape((2, 5, 2)),
+            lambda x: (np.ones(3), -0.1),
+        ]
+
+        args_constant = [
+            (0.1, torch.tensor([-2.1, 0.1])),
+            (1.2,),
+            (torch.ones((2, 3)),),
+        ]
+
+        @qml.qnode(dev, interface=interface)
+        def dependent_circuit(x, y, z):
+            qml.RX(0.1, wires=0)
+            qml.RY(y / 2, wires=0)
+            qml.RZ(qml.math.sin(z), wires=0)
+            return qml.expval(qml.PauliX(0))
+
+        dependent_functions = [
+            dependent_circuit,
+            torch.tensor,
+            lambda x: (1 + qml.math.tanh(1000 * x)) / 2,
+            *dependent_lambdas,
+        ]
+
+        args_dependent = [
+            (0.1, torch.tensor([-2.1, 0.1]), -0.9),
+            (-4.1,),
+            (torch.ones((3, 8)) * 1.1,),
+            *args_dependent_lambdas,
+        ]
+
+        dependent_expect_torch_fail = [False, False, False, *lambdas_expect_torch_fail]
+
+        @pytest.mark.parametrize("func, args", zip(constant_functions, args_constant))
+        def test_independent(self, func, args):
+            """Tests that an independent function is correctly detected as such."""
+            assert is_independent(func, self.interface, args)
+
+        @pytest.mark.parametrize(
+            "func, args, exp_fail",
+            zip(dependent_functions, args_dependent, dependent_expect_torch_fail),
+        )
+        def test_dependent(self, func, args, exp_fail):
+            """Tests that a dependent function is correctly detected as such."""
+            if exp_fail:
+                assert is_independent(func, self.interface, args)
+            else:
+                assert not is_independent(func, self.interface, args)
+
+        @pytest.mark.xfail
+        @pytest.mark.parametrize("func, args", zip(overlooked_lambdas, args_overlooked_lambdas))
+        def test_overlooked_dependence(self, func, args):
+            """Test that particular functions that are dependent on the input
+            are overlooked."""
+            assert not is_independent(func, self.interface, args)
+
+        def test_kwargs_are_considered(self):
+            """Tests that kwargs are taken into account when checking
+            independence of outputs."""
+            f = lambda x, kw=False: 0.1 * x if kw else 0.2
+            jac = lambda x, kw: torch.autograd.functional.jacobian(lambda x: f(x, kw), x)
+            args = (torch.tensor(0.2),)
+            assert is_independent(f, self.interface, args)
+            assert not is_independent(f, self.interface, args, {"kw": True})
+            assert is_independent(jac, self.interface, args, {"kw": True})
+
+
+class TestOther:
+    """Other tests for is_independent."""
+
+    def test_unknown_interface(self):
+        """Test that an error is raised if an unknown interface is requested."""
+        with pytest.raises(ValueError, match="Unknown interface: hello"):
+            is_independent(lambda x: x, "hello", (0.1,))