Repository: PennyLaneAI/pennylane
Pull request #6989 (2025-02-20 23:04:46)

add NullMeasurement
**Context:**

Performing measurements can consume a non-trivial amount of resources on large problems.  We need to be able to have a dummy measurement process that allows us to use the full pennylane workflow, without spending any amount of resources processing a measurement result.

This will allow us to perform benchmarks on large and remote devices without actually needing to do any additional meausrements.

**Description of the Change:**

Adds a `qml.measurements.NullMeasurement` measurement process that always corresponds to `np.array(np.nan)`, with a shape `()` and a dtype `float`. This was chosen so the measurement could be used with jax, program capture, and catalyst.

**Benefits:**

More informative profiles.

**Possible Drawbacks:**

Don't know if it's useful outside of profiling.  Would be nice to have a `None` result, but that doesn't have a shape and dtype.

**Related GitHub Issues:**

[sc-52374]

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6989?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.60%. Comparing base [(`bf24582`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/bf245828eba84286b75a3950a39dafddff6e9985?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`a7688a2`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/a7688a269d601956ac2f06be26578ab126281ac5?dropdown=coverage&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    #6989   +/-   ##
=======================================
  Coverage   99.60%   99.60%           
=======================================
  Files         481      482    +1     
  Lines       45918    45937   +19     
=======================================
+ Hits        45735    45754   +19     
  Misses        183      183           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6989?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 d1797af5d..521ac70fc 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -37,6 +37,10 @@
 
 <h3>Improvements 🛠</h3>
 
+* The `qml.measurements.NullMeasurement` measurement process is added to allow for profiling problems
+  without the overheads associated with performing measurements.
+  [(#6989)](https://github.com/PennyLaneAI/pennylane/pull/6989)
+
 * `pauli_rep` property is now accessible for `Adjoint` operator when there is a Pauli representation.
   [(#6871)](https://github.com/PennyLaneAI/pennylane/pull/6871)
 
diff --git a/pennylane/measurements/__init__.py b/pennylane/measurements/__init__.py
index 3ffbd3fc2..4aeb4e4f5 100644
--- a/pennylane/measurements/__init__.py
+++ b/pennylane/measurements/__init__.py
@@ -297,6 +297,7 @@ from .mid_measure import (
     get_mcm_predicates,
 )
 from .mutual_info import MutualInfoMP, mutual_info
+from .null_measurement import NullMeasurement
 from .probs import ProbabilityMP, probs
 from .purity import PurityMP, purity
 from .sample import SampleMP, sample
diff --git a/pennylane/measurements/null_measurement.py b/pennylane/measurements/null_measurement.py
new file mode 100644
index 000000000..ad1fc816d
--- /dev/null
+++ b/pennylane/measurements/null_measurement.py
@@ -0,0 +1,60 @@
+# Copyright 2018-2025 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.
+# pylint: disable=protected-access
+"""
+This module contains the qml.mutual_info measurement.
+"""
+import numpy as np
+
+from .measurements import SampleMeasurement, StateMeasurement
+
+
+class NullMeasurement(SampleMeasurement, StateMeasurement):
+    """A measurement that strictly returns an array with one nan.
+
+    This measurement is for profiling problems without the overhead of performing a measurement.
+
+    >>> @qml.qnode(qml.device('default.qubit', wires=1), diff_method="parameter-shift")
+    ... def circuit():
+    ...     return qml.measurements.NullMeasurement()
+    ...
+    >>> circuit()
+    array(nan)
+
+    ``np.array(np.nan)`` is chosen so the result still has a shape and data type for integration
+    with jax, catalyst, and program capture.
+
+    """
+
+    _shortname = "null"
+    numeric_type = float
+
+    @classmethod
+    def _abstract_eval(cls, *_, **__):
+        return (), float
+
+    def shape(self, *_, **__):
+        return ()
+
+    def process_density_matrix(self, *_, **__):
+        return np.array(np.nan)
+
+    def process_samples(self, *_, **__):
+        return np.array(np.nan)
+
+    def process_counts(self, *_, **__):
+        return np.array(np.nan)
+
+    def process_state(self, *_, **__):
+        return np.array(np.nan)


Test patch:
diff --git a/tests/measurements/test_null_measurement.py b/tests/measurements/test_null_measurement.py
new file mode 100644
index 000000000..1b443f727
--- /dev/null
+++ b/tests/measurements/test_null_measurement.py
@@ -0,0 +1,75 @@
+# Copyright 2018-2025 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 mutual_info module"""
+# pylint: disable=use-implicit-booleaness-not-comparison
+import numpy as np
+import pytest
+
+import pennylane as qml
+from pennylane.measurements.null_measurement import NullMeasurement
+
+
+@pytest.mark.parametrize(
+    "method", ("process_density_matrix", "process_state", "process_counts", "process_samples")
+)
+def test_null_measurement_process_methods(method):
+    """Test that all process_methods return `np.nan`"""
+
+    mp = NullMeasurement()
+    out = getattr(mp, method)()
+    assert np.isnan(out)
+    assert out.shape == ()
+    assert out.dtype == np.float64
+
+
+def test_shape_and_dtype_null_measurement():
+    """Test that the shape of the shape and dtype of a null measurement are correct."""
+
+    mp = NullMeasurement()
+
+    assert mp.shape() == ()
+    assert mp.numeric_type == float
+    assert mp._abstract_eval() == ((), float)  # pylint: disable=protected-access
+
+
+@pytest.mark.jax
+def test_integration_jax_jit():
+    """Test that execution of the null measurement works with jitting."""
+    import jax
+
+    @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift")
+    def c(x):
+        qml.RX(x, 0)
+        return NullMeasurement()
+
+    r = jax.jit(c)(jax.numpy.array(0.5))
+    assert np.isnan(r)
+    assert r.shape == ()
+    assert r.dtype == np.float64
+
+
+# pylint: disable=unused-argument
+@pytest.mark.jax
+def test_capture(enable_disable_plxpr):
+    """Test that null measurement works with plxpr."""
+
+    @qml.qnode(qml.device("default.qubit", wires=1))
+    def c(x):
+        qml.RX(x, 0)
+        return NullMeasurement()
+
+    out = c(0.5)
+    assert np.isnan(out)
+    assert out.shape == ()
+    assert out.dtype == np.float64