Skip to content

Combine counters #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion povm_toolbox/post_processor/median_of_means.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def __init__(
povm_sample: POVMPubResult,
dual: BaseDual | None = None,
*,
combine_counts: bool = False,
num_batches: int | None = None,
upper_delta_confidence: float | None = None,
seed: int | Generator | None = None,
Expand All @@ -90,6 +91,9 @@ def __init__(
:meth:`get_decomposition_weights`. When this is ``None``, the default
"state-average" Dual frame will be constructed from the POVM stored in the
``povm_sample``'s :attr:`.POVMPubResult.metadata`.
combine_counts: indicates, when applicable, whether to combine the counts associated
with different parameter values that were submitted for a single parametrized
circuit.
Comment on lines +94 to +96
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be aligned with whatever updates we apply based on the previous comment.

num_batches: number of batches, i.e. number of samples means, used in the median-of-means
estimator. This value will be overridden if a ``delta_confidence`` argument is supplied.
upper_delta_confidence: an upper bound for the confidence parameter :math:`\delta` used to
Expand All @@ -107,7 +111,7 @@ def __init__(
``povm_samples``'s :attr:`.POVMPubResult.metadata`.
TypeError: If the type of ``seed`` is not valid.
"""
super().__init__(povm_sample=povm_sample, dual=dual)
super().__init__(povm_sample=povm_sample, dual=dual, combine_counts=combine_counts)

self.num_batches: int
"""Number of batches, i.e. number of samples means, used in the median-of-means estimator."""
Expand Down
11 changes: 11 additions & 0 deletions povm_toolbox/post_processor/povm_post_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from __future__ import annotations

import logging
from collections import Counter
from typing import Any, cast

import numpy as np
Expand Down Expand Up @@ -60,6 +61,8 @@ def __init__(
self,
povm_sample: POVMPubResult,
dual: BaseDual | None = None,
*,
combine_counts: bool = False,
) -> None:
"""Initialize the POVM post-processor.

Expand All @@ -70,6 +73,9 @@ def __init__(
:meth:`get_decomposition_weights`. When this is ``None``, the standard
"state-average" Dual frame will be constructed from the POVM stored in the
``povm_sample``'s :attr:`.POVMPubResult.metadata`.
combine_counts: indicates, when applicable, whether to combine the counts associated
with different parameter values that were submitted for a single parametrized
circuit.
Comment on lines +76 to +78
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this explanation has to be extended further:

  • we should clarify what "when applicable" means
  • we should clarify that and how we use np.ndarray.flatten
  • we should have a clear example what this implies for 0d-, 1d-, and Nd- arrays of parameters in the original job

We can either do that in the docs here, or in a how-to guide and link to that from here (which may be better as it gives us more space for explanations).


Raises:
ValueError: If the provided ``dual`` is not a dual frame to the POVM stored in the
Expand All @@ -78,6 +84,11 @@ def __init__(
self._povm = povm_sample.metadata.povm_implementation.definition()

self._counts = cast(np.ndarray, povm_sample.get_counts())
if combine_counts:
combined_counter: Counter = Counter()
for count in self._counts.flatten():
combined_counter.update(count)
self._counts = np.array([combined_counter], dtype=object)

if (dual is not None) and (not dual.is_dual_to(self._povm)):
raise ValueError(
Expand Down
10 changes: 5 additions & 5 deletions povm_toolbox/sampler/povm_pub_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,18 @@ def metadata(self) -> POVMMetadata:
return self._metadata # type:ignore

def get_counts(self, *, loc: int | tuple[int, ...] | None = None) -> np.ndarray | Counter:
"""Get the histogram data of the result.
"""Get the counter of outcomes from the result.

This method will leverage :meth:`~.POVMImplementation.get_povm_counts_from_raw` from the
:class:`.POVMImplementation` instance stored inside the :attr:`metadata` to construct a
histogram of POVM outcomes.
counter of POVM outcomes.

Args:
loc: Which entry of the :class:`~qiskit.primitives.containers.bit_array.BitArray` to
return a histogram for. If the Pub that was submitted to :meth:`.POVMSampler.run`
return a counter for. If the pub that was submitted to :meth:`.POVMSampler.run`
contained circuit parameters, ``loc`` can be used to indicate the set of parameter
values for which to compute the histogram. If ``loc is None``, the histogram will be
computed for all parameter values at once.
values for which to compute the counter. If ``loc`` is ``None``, the histogram
will be computed for all parameter values at once.

Returns:
Either a single or an array of histograms of the POVM outcomes. The shape depends on the
Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/combine-counts-9bd0d6639398af44.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
Add an option for the post-processors to combine the counts associated with different parameter
values that were submitted in the same POVM sampler PUB.
Comment on lines +4 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do end up with a new how-to guide, we should link to that from here.


34 changes: 24 additions & 10 deletions test/post_processor/test_post_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,24 @@ def test_get_expectation_value(self):
exp_val, std = post_processor.get_expectation_value(observable, loc=0)
self.assertAlmostEqual(exp_val, -1.6406249999999998)
self.assertAlmostEqual(std, 1.3442744428582185)

def test_get_expectation_value_parametrized_circuit(self):
"""Test that the ``get_expectation_value`` method works correctly with parametrized circuit."""
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.ry(theta=Parameter("theta"), qubit=0)
povm_sampler = POVMSampler(sampler=Sampler(seed=self.SEED))
measurement = ClassicalShadows(num_qubits=2, seed=self.SEED)
job = povm_sampler.run(
[(qc, np.array(2 * [[0, np.pi / 3, np.pi]]))], shots=32, povm=measurement
)
pub_result = job.result()[0]
observable = SparsePauliOp(["IZ", "XX", "ZY"], coeffs=[-0.5, 1, -2])
with self.subTest("Test with default ``loc`` for parametrized circuit."):
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.ry(theta=Parameter("theta"), qubit=0)
povm_sampler = POVMSampler(sampler=Sampler(seed=self.SEED))
measurement = ClassicalShadows(num_qubits=2, seed=self.SEED)
job = povm_sampler.run(
[(qc, np.array(2 * [[0, np.pi / 3, np.pi]]))], shots=32, povm=measurement
)
pub_result = job.result()[0]
post_processor = POVMPostProcessor(pub_result)
self.assertEqual(post_processor.counts.shape, (2, 3))
self.assertEqual(sum(post_processor.counts[0, 0].values()), 32)
exp_val, std = post_processor.get_expectation_value(observable)
self.assertIsInstance(exp_val, np.ndarray)
self.assertEqual(exp_val.shape, (2, 3))
Expand Down Expand Up @@ -158,6 +164,14 @@ def test_get_expectation_value(self):
),
)
)
with self.subTest("Test with combining counts."):
post_processor = POVMPostProcessor(pub_result, combine_counts=True)
self.assertEqual(post_processor.counts.shape, (1,))
self.assertEqual(sum(post_processor.counts[0].values()), 2 * 3 * 32)
exp_val, std = post_processor.get_expectation_value(observable)
self.assertIsInstance(exp_val, float)
self.assertAlmostEqual(exp_val, -1.5546875)
self.assertAlmostEqual(std, 0.47945849433203297)

def test_single_exp_value_and_std(self):
"""Test that the ``_single_exp_value_and_std`` method works correctly."""
Expand Down