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

Merged
merged 23 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
525 changes: 525 additions & 0 deletions docs/how_tos/combine_outcomes.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/how_tos/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This page will summarize the available how-to guides.
non_ic_measurements
visualization
layout
combine_outcomes
job_recovery
dual_optimizer
dilation_measurements
Expand Down
10 changes: 5 additions & 5 deletions docs/tutorials/parametrized_circuit.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"metadata": {},
"outputs": [
{
Expand All @@ -161,7 +161,7 @@
"\n",
"# Note that the pub result will contain a `BitArray` that has the same shape\n",
"# as the submitted `BindingsArray`, which is (2,) in this example.\n",
"print(pub_result.get_counts().shape)"
"print(pub_result.get_counts(loc=...).shape)"
]
},
{
Expand Down Expand Up @@ -273,7 +273,7 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": null,
"metadata": {},
"outputs": [
{
Expand All @@ -287,7 +287,7 @@
"source": [
"job = povm_sampler.run([(qc, theta, shots, cs_implementation)])\n",
"pub_result = job.result()[0]\n",
"print(pub_result.get_counts().shape)"
"print(pub_result.get_counts(loc=...).shape)"
]
},
{
Expand Down Expand Up @@ -332,7 +332,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
"version": "3.12.11"
}
},
"nbformat": 4,
Expand Down
4 changes: 2 additions & 2 deletions docs/tutorials/povm_sampler_pub.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": null,
"metadata": {},
"outputs": [
{
Expand All @@ -447,7 +447,7 @@
],
"source": [
"for pub_result in result:\n",
" print(pub_result.get_counts())"
" print(pub_result.get_counts(loc=...))"
]
},
{
Expand Down
104 changes: 72 additions & 32 deletions povm_toolbox/library/povm_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@
from __future__ import annotations

import logging
import sys
import time
from abc import ABC, abstractmethod
from collections import Counter
from copy import copy
from typing import TYPE_CHECKING, Generic, TypeVar, cast

if sys.version_info < (3, 10):
from typing import Any

# There is no way to support this type properly in python 3.9, which will be end of life in
# November 2025 anyways.
EllipsisType = Any
else:
from types import EllipsisType

import numpy as np
from qiskit.circuit import AncillaRegister, QuantumCircuit
from qiskit.circuit.exceptions import CircuitError
Expand Down Expand Up @@ -151,7 +161,7 @@ def compose_circuits(self, circuit: QuantumCircuit) -> QuantumCircuit:
circuit: The quantum circuit to be sampled from.

Raises:
ValueError: if the number of qubits specified by `self.measurement_layout` does not
ValueError: if the number of qubits specified by ``self.measurement_layout`` does not
match the number of qubits on which this POVM implementation acts.
CircuitError: if an error has occurred when adding the classic register, used to save
POVM results, to the input circuit.
Expand Down Expand Up @@ -274,14 +284,22 @@ def _povm_outcomes(
bit_array: BitArray,
povm_metadata: MetadataT,
*,
loc: int | tuple[int, ...] | None = None,
loc: tuple[int, ...],
) -> list[tuple[int, ...]]:
"""Convert the raw bitstrings into POVM outcomes based on the associated metadata.

Args:
bit_array: The raw bitstrings.
povm_metadata: The associated metadata.
loc: an optional location to slice the bitstrings.
loc: index of the element of the ``bit_array`` from which return the outcomes. More
precisely, ``bit_array`` has the shape of the array of parameter sets provided to this
``POVMSamplerPub`` and ``loc`` must specify exactly one of the parameter set. Therefore,
we must have ``len(loc)==len(bit_array.shape)``. Especially, if no parameter sets were
provided in the pub -- i.e., the quantum circuit was not parametrized or already
bound -- then ``loc`` must be an empty tuple ``(,)``.

Raises:
ValueError: if ``loc`` is not compatible with the shape of ``bit_array``.

Returns:
The converted POVM outcomes.
Expand All @@ -292,59 +310,81 @@ def get_povm_counts_from_raw(
data: DataBin,
povm_metadata: MetadataT,
*,
loc: int | tuple[int, ...] | None = None,
loc: int | tuple[int, ...] | EllipsisType | None = None,
) -> np.ndarray | Counter:
"""Get the POVM counts.

Args:
data: The raw sampled data.
povm_metadata: The associated metadata.
loc: an optional location to slice the bitstrings.
loc: specifies the location of the counts to return. By default, ``None`` is used, which
aggregates all counts from a single PUB. If ``loc=...``, all counts from the PUB are
returned, but separately. If ``loc`` is a tuple of integers, it must define a single
parameter set. Refer to
`this how-to guide <../how_tos/combine_outcomes.ipynb>`_ for more information.

Returns:
The POVM counts.
The POVM counts. If ``loc=...``, an ``np.ndarray`` of counters is returned. Otherwise, a
single counter is returned.
"""
bit_array = self._get_bitarray(data)

if loc is not None:
return Counter(self._povm_outcomes(bit_array, povm_metadata, loc=loc))
samples = self.get_povm_outcomes_from_raw(data, povm_metadata, loc=loc)

if bit_array.ndim == 0:
return cast(
np.ndarray,
np.asarray([Counter(self._povm_outcomes(bit_array, povm_metadata))], dtype=object),
)
if loc is Ellipsis:
# When ``loc`` is an ``Ellipsis``, samples is guaranteed to be an ``np.ndarray``
samples = cast(np.ndarray, samples)
shape = samples.shape
outcomes_array: np.ndarray = np.ndarray(shape=shape, dtype=object)
for idx in np.ndindex(shape):
outcomes_array[idx] = Counter(samples[idx])
return outcomes_array

shape = bit_array.shape
outcomes_array: np.ndarray = np.ndarray(shape=shape, dtype=object)
for idx in np.ndindex(shape):
outcomes_array[idx] = Counter(self._povm_outcomes(bit_array, povm_metadata, loc=idx))
return outcomes_array
return Counter(samples)

def get_povm_outcomes_from_raw(
self,
data: DataBin,
povm_metadata: MetadataT,
*,
loc: int | tuple[int, ...] | None = None,
loc: int | tuple[int, ...] | EllipsisType | None = None,
) -> np.ndarray | list[tuple[int, ...]]:
"""Get the POVM bitstrings.
"""Get the POVM outcomes.

Args:
data: The raw sampled data.
povm_metadata: The associated metadata.
loc: an optional location to slice the bitstrings.
loc: specifies the location of the outcomes to return. By default, ``None`` is used,
which aggregates all outcomes from a single PUB. If ``loc=...``, all outcomes from
the PUB are returned, but separately. If ``loc`` is a tuple of integers, it must
define a single parameter set. Refer to
`this how-to guide <../how_tos/combine_outcomes.ipynb>`_ for more information.

Returns:
The POVM bitstrings.
The list of POVM outcomes. If ``loc=...``, an ``np.ndarray`` of outcome lists is returned.
Otherwise, a single outcome list is returned.
"""
bit_array = self._get_bitarray(data)

if loc is not None or bit_array.ndim == 0:
return self._povm_outcomes(bit_array, povm_metadata, loc=loc)

shape = bit_array.shape
outcomes_array: np.ndarray = np.ndarray(shape=shape, dtype=object)
for idx in np.ndindex(shape):
outcomes_array[idx] = self._povm_outcomes(bit_array, povm_metadata, loc=idx)
return outcomes_array
if loc is None:
outcomes: list[tuple[int, ...]] = []
for idx in np.ndindex(bit_array.shape):
outcomes.extend(self._povm_outcomes(bit_array, povm_metadata, loc=idx))
return outcomes

if loc is Ellipsis:
outcomes_array: np.ndarray
if bit_array.ndim == 0:
outcomes_array = np.ndarray(shape=(1,), dtype=object)
loc = tuple()
outcomes_array[0] = self._povm_outcomes(bit_array, povm_metadata, loc=loc)
return outcomes_array

shape = bit_array.shape

outcomes_array = np.ndarray(shape=shape, dtype=object)
for idx in np.ndindex(shape):
outcomes_array[idx] = self._povm_outcomes(bit_array, povm_metadata, loc=idx)
return outcomes_array

if isinstance(loc, int):
loc = (loc,)
return self._povm_outcomes(bit_array, povm_metadata, loc=loc)
6 changes: 3 additions & 3 deletions povm_toolbox/library/randomized_projective_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ def _povm_outcomes(
bit_array: BitArray,
povm_metadata: RPMMetadata,
*,
loc: int | tuple[int, ...] | None = None,
loc: tuple[int, ...],
) -> list[tuple[int, ...]]:
t1 = time.time()
LOGGER.info("Creating POVM outcomes")
Expand All @@ -397,7 +397,7 @@ def _povm_outcomes(
# loc is assumed to have a length of at most pv.ndim = len(pv.shape)

try:
pvm_keys = povm_metadata.pvm_keys if loc is None else povm_metadata.pvm_keys[loc]
pvm_keys = povm_metadata.pvm_keys[loc]
except AttributeError as exc:
raise AttributeError(
"The metadata of povm sampler result associated with a "
Expand Down Expand Up @@ -533,7 +533,7 @@ def _get_outcome_label(
the index goes from :math:``0`` to :math:``2 * self.num_PVM - 1``.
"""
return tuple(
(pvm_idx[i] % self._num_PVMs) * 2 + (int(bit) + pvm_idx[i] // self._num_PVMs) % 2
int((pvm_idx[i] % self._num_PVMs) * 2 + (int(bit) + pvm_idx[i] // self._num_PVMs) % 2)
for i, bit in enumerate(bitstring_outcome[::-1])
)

Expand Down
7 changes: 6 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,10 @@ 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 whether to combine the counts associated with different
parameter sets that were submitted in a single :attr:`.POVMSamplerPub`. By default,
the counts are not combined. Refer to this
`how-to guide <../how_tos/combine_outcomes.ipynb>`_ for more information.
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 +112,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
12 changes: 11 additions & 1 deletion povm_toolbox/post_processor/povm_post_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ def __init__(
self,
povm_sample: POVMPubResult,
dual: BaseDual | None = None,
*,
combine_counts: bool = False,
) -> None:
"""Initialize the POVM post-processor.

Expand All @@ -70,14 +72,22 @@ 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 whether to combine the counts associated with different
parameter sets that were submitted in a single :attr:`.POVMSamplerPub`. By default,
the counts are not combined. Refer to this
`how-to guide <../how_tos/combine_outcomes.ipynb>`_ for more information.

Raises:
ValueError: If the provided ``dual`` is not a dual frame to the POVM stored in the
``povm_samples``'s :attr:`.POVMPubResult.metadata`.
"""
self._povm = povm_sample.metadata.povm_implementation.definition()

self._counts = cast(np.ndarray, povm_sample.get_counts())
self._counts: np.ndarray
if combine_counts:
self._counts = np.asarray([povm_sample.get_counts(loc=None)], dtype=object)
else:
self._counts = cast(np.ndarray, povm_sample.get_counts(loc=...))

if (dual is not None) and (not dual.is_dual_to(self._povm)):
raise ValueError(
Expand Down
Loading