Repository: PennyLaneAI/pennylane
Pull request #4739 (2023-10-26 01:05:41)

Update counts to handle floats/NaN values from hardware
The QuEra hardware sometimes has failed measurements, which return as NaN. Because of this, all samples from the device are returned with type `float`, and some of them are `np.NaN`. These were not handled well in counts. On the `_default_qubit` device, nothing was converted to integers, so it did this:

```
{'0.00.00.0': 149,
'0.00.01.0': 281, 
'0.00.0nan': 2,
'0.01.00.0': 277,
'0.01.01.0': 4,
'0.0nan0.0': 4,
'1.00.00.0': 268,
'1.00.0nan': 2,
'1.01.00.0': 5,
'nan0.00.0': 8}
```

We don't like that. Now they are converted into integers if there is no observable, just like in `CountsMP`.

Also added a line both there and in `CountsMP` to avoid converting the `NaN` values to 0 and implying they were ground state measurements - instead, any sample where at least one wire failed is disregarded when totalling the number of counts. 

Has been tested with the QuEra hardware, and now returns nicely:

```
>>> res = circuit(params)
>>> res
{'000': 230, '001': 251, '010': 252, '011': 1, '100': 249, '110': 4}
```

with 13 disregarded failed measurements:
```
>>> sum(count for count in res.values())
987
```



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/4739?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:
> Comparison is base [(`5cd8354`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/5cd8354d218bf4fdfc6b280fc5288badec511308?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64% compared to head [(`7571964`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4739?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64%.
> Report is 2 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master    #4739      +/-   ##
==========================================
- Coverage   99.64%   99.64%   -0.01%     
==========================================
  Files         380      380              
  Lines       34245    33996     -249     
==========================================
- Hits        34124    33874     -250     
- Misses        121      122       +1     
```


| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4739?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/\_qubit\_device.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4739?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19xdWJpdF9kZXZpY2UucHk=) | `99.44% <100.00%> (+<0.01%)` | :arrow_up: |
| [pennylane/pulse/transmon.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4739?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3B1bHNlL3RyYW5zbW9uLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/templates/subroutines/hilbert\_schmidt.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4739?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RlbXBsYXRlcy9zdWJyb3V0aW5lcy9oaWxiZXJ0X3NjaG1pZHQucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/transforms/core/transform\_dispatcher.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4739?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY29yZS90cmFuc2Zvcm1fZGlzcGF0Y2hlci5weQ==) | `100.00% <100.00%> (ø)` | |

... and [55 files with indirect coverage changes](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4739/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4739?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).

lillian542: [sc-48588]

Patch:
diff --git a/doc/releases/changelog-0.33.0.md b/doc/releases/changelog-0.33.0.md
index 1ea85bd82..c8fab4487 100644
--- a/doc/releases/changelog-0.33.0.md
+++ b/doc/releases/changelog-0.33.0.md
@@ -473,6 +473,11 @@
   device.
   [(#4736)](https://github.com/PennyLaneAI/pennylane/pull/4736)
 
+* `QubitDevice` and `CountsMP` are updated to disregard samples containing failed hardware measurements
+  (record as `np.NaN`) when tallying samples, rather than counting failed measurements as ground-state 
+  measurements, and to display `qml.counts` coming from these hardware devices correctly.
+  [(#4739)](https://github.com/PennyLaneAI/pennylane/pull/4739)
+
 <h3>Breaking changes 💔</h3>
 
 * `qml.defer_measurements` now raises an error if a transformed circuit measures `qml.probs`,
diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py
index e1354655e..f15d0f1f1 100644
--- a/pennylane/_qubit_device.py
+++ b/pennylane/_qubit_device.py
@@ -1437,6 +1437,8 @@ class QubitDevice(Device):
 
         if isinstance(obs, CountsMP):
             # convert samples and outcomes (if using) from arrays to str for dict keys
+            samples = np.array([sample for sample in samples if not np.any(np.isnan(sample))])
+            samples = qml.math.cast_like(samples, qml.math.int8(0))
             samples = np.apply_along_axis(_sample_to_str, -1, samples)
             batched_ndims = 3  # no observable was provided, batched samples will have shape (batch_size, shots, len(wires))
             if obs.all_outcomes:
@@ -1484,7 +1486,6 @@ class QubitDevice(Device):
             Union[array[float], dict, list[dict]]: samples in an array of
             dimension ``(shots,)`` or counts
         """
-
         # translate to wire labels used by device
         device_wires = self.map_wires(observable.wires)
         name = observable.name
diff --git a/pennylane/measurements/counts.py b/pennylane/measurements/counts.py
index 58633f8e8..6c7b0de36 100644
--- a/pennylane/measurements/counts.py
+++ b/pennylane/measurements/counts.py
@@ -294,6 +294,9 @@ class CountsMP(SampleMeasurement):
 
         if self.obs is None:
             # convert samples and outcomes (if using) from arrays to str for dict keys
+            samples = qml.math.array(
+                [sample for sample in samples if not qml.math.any(qml.math.isnan(sample))]
+            )
             samples = qml.math.cast_like(samples, qml.math.int8(0))
             samples = qml.math.apply_along_axis(_sample_to_str, -1, samples)
             batched_ndims = 3  # no observable was provided, batched samples will have shape (batch_size, shots, len(wires))


Test patch:
diff --git a/tests/measurements/test_counts.py b/tests/measurements/test_counts.py
index a866eab96..33263091c 100644
--- a/tests/measurements/test_counts.py
+++ b/tests/measurements/test_counts.py
@@ -128,6 +128,26 @@ class TestProcessSamples:
             np.logical_and(samples[:, 0] == 1, samples[:, 1] == 1)
         )
 
+    def test_counts_with_nan_samples(self):
+        """Test that the counts function disregards failed measurements (samples including
+        NaN values) when totalling counts"""
+        shots = 1000
+        samples = np.random.choice([0, 1], size=(shots, 2)).astype(np.float64)
+
+        samples[0][0] = np.NaN
+        samples[17][1] = np.NaN
+        samples[850][0] = np.NaN
+
+        result = qml.counts(wires=[0, 1]).process_samples(samples, wire_order=[0, 1])
+
+        # no keys with NaNs
+        assert len(result) == 4
+        assert set(result.keys()) == {"00", "01", "10", "11"}
+
+        # NaNs were not converted into "0", but were excluded from the counts
+        total_counts = sum(count for count in result.values())
+        assert total_counts == 997
+
     def test_counts_obs(self):
         """Test that the counts function outputs counts of the right size for observables"""
         shots = 1000
diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py
index ade01093c..824b62fb2 100644
--- a/tests/test_qubit_device.py
+++ b/tests/test_qubit_device.py
@@ -1456,3 +1456,29 @@ class TestResourcesTracker:
             expected_resources,
             expected_resources,
         ]
+
+
+def test_samples_to_counts_with_nan():
+    """Test that the counts function disregards failed measurements (samples including
+    NaN values) when totalling counts"""
+    # generate 1000 samples for 2 wires, randomly distributed between 0 and 1
+    device = qml.device("default.qubit.legacy", wires=2, shots=1000)
+    device._state = [0.5 + 0.0j, 0.5 + 0.0j, 0.5 + 0.0j, 0.5 + 0.0j]
+    device._samples = device.generate_samples()
+    samples = device.sample(qml.measurements.CountsMP())
+
+    # imitate hardware return with NaNs (requires dtype float)
+    samples = qml.math.cast_like(samples, np.array([1.2]))
+    samples[0][0] = np.NaN
+    samples[17][1] = np.NaN
+    samples[850][0] = np.NaN
+
+    result = device._samples_to_counts(samples, obs=qml.measurements.CountsMP(), num_wires=2)
+
+    # no keys with NaNs
+    assert len(result) == 4
+    assert set(result.keys()) == {"00", "01", "10", "11"}
+
+    # # NaNs were not converted into "0", but were excluded from the counts
+    total_counts = sum(count for count in result.values())
+    assert total_counts == 997