Repository: PennyLaneAI/pennylane
Pull request #4952 (2023-12-15 14:49:17)

Feature/implement probability mp.process counts #4943
### Pull request - issue #4943

**Context: This pull request implements the new feature described in issue #4943 (internal assignment).**

**Description of the Change:**
Implemented the method `process_counts` in the `ProbabilityMP` class to compute a probability measurement from a counts dictionary.

**Benefits:**
Representation of store sampling information in the form of a dictionary counts is much more condensed and memory efficient.

**Possible Drawbacks:** 
The new method should reproduce the results of `process_samples`, implemented in the same class. It has been tested extensively, although I still need to become an expert in quantum computing and circuits. 
The function checks that the provided sampling information is a dictionary matching the format returned by `CountsMP`.

**Related GitHub Issues:** 
#4943 

----------------------------------------------------------------------------------------------------------------------------------------------------------------------

**Further details:** 

First of all, congratulations on the library and your fantastic work! 

I had the time to explore and play with the code, which is quite amazing. I hope to be considered worthy and help you maintain, expand, and improve it.

I am confident you have improvements and interesting suggestions regarding my pull request. I left some questions for you below as a comment on this PR.

Since this is an internal assignment (for which I was given a deadline), I include some information below that might be relevant for your evaluation (time zone: GMT-5):

- *Start time:* December 15th, 9:07 AM 
- *Marked as ready for review on:* December 18th, 3:53 AM
- *End time:* TBD
- *Deadline:* December 29th, 9:07 AM

I am incredibly grateful for your time, and I hope to learn so much from all of you!
I wish you all the best.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4952?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 [(`837c5a2`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/837c5a210d20960d654bc8c4ea66d9e8c78bee6b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.50% compared to head [(`96c7469`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4952?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.49%.


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


```diff
@@            Coverage Diff             @@
##           master    #4952      +/-   ##
==========================================
- Coverage   99.50%   99.49%   -0.01%     
==========================================
  Files         390      390              
  Lines       35533    35265     -268     
==========================================
- Hits        35357    35088     -269     
- Misses        176      177       +1     
```



</details>

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

PietropaoloFrisoni: As I mentioned above, I have a few elementary questions for you:

- I implemented a few checks on the `counts` parameter to verify that it is a dictionary matching the format returned by class:`CountsMP,` defined in `pennylane.measurements.counts.py`. I noticed that there are no similar checks in other methods. Is this a deliberate choice? If that's the case, please tell me if I should remove such checks (or if I should add more of them).
- I understand that the sampling information should be provided so that keys correspond to measurement outcomes in binary strings, like `00010`, etc., and no other digit is allowed. Please let me know if that's not the case, and I'll change the code.
- Finally, just a comment. Since I pushed the last changes, the test associated with `codecov/project` seems stuck, although it was perfectly fine in the previous commit(s). I suspect it is most probably a `codecov` issue.

If you want to test the code and don't want to write something from scratch, I leave you a very basic snippet I used during some of my tests.

I wish you all the best once again!

```
@qml.qnode(qml.device("default.qubit"))
def circuit():
    qml.Hadamard(wires=1)
    qml.Hadamard(wires=2)
    qml.CNOT(wires=[0, 1])
    return qml.sample(wires=(0, 1, 2))


wires = (0, 1)
wire_order = qml.wires.Wires((1, 0))

samples = circuit(shots=100)
counts = {}

for i in range(samples.shape[0]):
    binary_string = "".join(map(str, samples[i, :]))
    counts[binary_string] = counts.get(binary_string, 0) + 1

print(f"samples = {samples}")
print(f"counts = {counts}\n")

prob_old = qml.probs(wires=wires).process_samples(samples, wire_order=wire_order)
prob_new = qml.probs(wires=wires).process_counts(counts, wire_order=wire_order)
(prob_old == prob_new).all()
```
PietropaoloFrisoni: Hi @albi3ro and @mudit2812.

Thank you so much for your review and your time!


> Sorry about the delay in getting back to you.
> 
No worries, I didn't want to bother you with reminders (especially during Christmas time)



> This is looking good 🎉
> 

> I left two minor comments, but other than that, this is exactly what I was looking for.

Thank you, Christina! 

I pushed a commit with your suggestion and removed the asserts as you both suggested.

If you are not ready to approve the PR, please tell me if there's something else you want me to modify.

Merry Christmas to both of you, as well as to everyone else on the team!
mudit2812: Regarding the test failure showing up right now. It's unrelated to your PR @PietropaoloFrisoni and there's no need to worry about it. The test validates a finite-shots workflow where the results are non-deterministic. I will resolve this in a different PR. In the mean time, you can re-run CI without having to make new changes by running `git commit --allow-empty -m 'Trigger CI'; git pull; git push`

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 90debbcc3..089a9ec66 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -293,6 +293,9 @@
 
 <h3>Improvements 🛠</h3>
 
+* Implemented the method `process_counts` in the `ProbabilityMP` class.
+  [(#4952)](https://github.com/PennyLaneAI/pennylane/pull/4952)
+
 * `ClassicalShadow.entropy` now uses the algorithm outlined in 
   [1106.5458](https://arxiv.org/abs/1106.5458) to project the approximate density matrix
   (with potentially negative eigenvalues) onto the closest valid density matrix.
@@ -669,6 +672,7 @@ Minh Chau,
 Isaac De Vlugt,
 Amintor Dusko,
 Lillian Frederiksen,
+Pietropaolo Frisoni,
 Josh Izaac,
 Juan Giraldo,
 Emiliano Godinez Ramirez,
diff --git a/pennylane/measurements/probs.py b/pennylane/measurements/probs.py
index b7fe0afd5..c1fe68bd4 100644
--- a/pennylane/measurements/probs.py
+++ b/pennylane/measurements/probs.py
@@ -241,6 +241,31 @@ class ProbabilityMP(SampleMeasurement, StateMeasurement):
         # flatten and return probabilities
         return qml.math.reshape(prob, flat_shape)
 
+    def process_counts(self, counts: dict, wire_order: Wires) -> np.ndarray:
+        wire_map = dict(zip(wire_order, range(len(wire_order))))
+        mapped_wires = [wire_map[w] for w in self.wires]
+
+        # when reducing wires, two keys may become equal
+        # the following structure was chosen to maintain compatibility with 'process_samples'
+        if mapped_wires:
+            mapped_counts = {}
+            for outcome, occurrence in counts.items():
+                mapped_outcome = "".join(outcome[i] for i in mapped_wires)
+                mapped_counts[mapped_outcome] = mapped_counts.get(mapped_outcome, 0) + occurrence
+            counts = mapped_counts
+
+        num_shots = sum(counts.values())
+        num_wires = len(next(iter(counts)))
+        dim = 2**num_wires
+
+        # constructs the probability vector
+        # converts outcomes from binary strings to integers (base 10 representation)
+        prob_vector = qml.math.zeros((dim), dtype="float64")
+        for outcome, occurrence in counts.items():
+            prob_vector[int(outcome, base=2)] = occurrence / num_shots
+
+        return prob_vector
+
     @staticmethod
     def _count_samples(indices, batch_size, dim):
         """Count the occurrences of sampled indices and convert them to relative


Test patch:
diff --git a/tests/measurements/test_probs.py b/tests/measurements/test_probs.py
index 37ae5251d..f3e1ee6cc 100644
--- a/tests/measurements/test_probs.py
+++ b/tests/measurements/test_probs.py
@@ -724,6 +724,29 @@ class TestProbs:
 
         assert np.allclose(res, expected)
 
+    @pytest.mark.parametrize(
+        "wires, expected",
+        [
+            (
+                (0, 1, 2),
+                [0.1, 0.2, 0.0, 0.1, 0.0, 0.2, 0.1, 0.3],
+            ),
+            (
+                (0, 1),
+                [0.3, 0.1, 0.2, 0.4],
+            ),
+        ],
+    )
+    def test_estimate_probability_with_counts(self, wires, expected):
+        """Tests the estimate_probability method with sampling information in the form of a counts dictionary"""
+        counts = {"101": 2, "100": 2, "111": 3, "000": 1, "011": 1, "110": 1}
+
+        wire_order = qml.wires.Wires((2, 1, 0))
+
+        res = qml.probs(wires=wires).process_counts(counts=counts, wire_order=wire_order)
+
+        assert np.allclose(res, expected)
+
     def test_non_commuting_probs_does_not_raises_error(self):
         """Tests that non-commuting probs with expval does not raise an error."""
         dev = qml.device("default.qubit", wires=5)