Skip to content

Commit

Permalink
Add SamplerPubResult (Qiskit#12143)
Browse files Browse the repository at this point in the history
* add SamplerPubResult

* add join_data

* Update qiskit/primitives/containers/sampler_pub_result.py

Co-authored-by: Ian Hincks <[email protected]>

* adding tests

* add join_data tests

* add reno

* fix linting

---------

Co-authored-by: Ian Hincks <[email protected]>
Co-authored-by: Ian Hincks <[email protected]>
  • Loading branch information
3 people authored May 2, 2024
1 parent 43c065f commit 849fa00
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 19 deletions.
12 changes: 6 additions & 6 deletions qiskit/primitives/backend_sampler_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
BitArray,
DataBin,
PrimitiveResult,
PubResult,
SamplerPubLike,
SamplerPubResult,
)
from qiskit.primitives.containers.bit_array import _min_num_bytes
from qiskit.primitives.containers.sampler_pub import SamplerPub
Expand Down Expand Up @@ -124,7 +124,7 @@ def options(self) -> Options:

def run(
self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None
) -> PrimitiveJob[PrimitiveResult[PubResult]]:
) -> PrimitiveJob[PrimitiveResult[SamplerPubResult]]:
if shots is None:
shots = self._options.default_shots
coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs]
Expand All @@ -142,7 +142,7 @@ def _validate_pubs(self, pubs: list[SamplerPub]):
UserWarning,
)

def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[PubResult]:
def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[SamplerPubResult]:
pub_dict = defaultdict(list)
# consolidate pubs with the same number of shots
for i, pub in enumerate(pubs):
Expand All @@ -157,7 +157,7 @@ def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[PubResult]:
results[i] = pub_result
return PrimitiveResult(results)

def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[PubResult]:
def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult]:
"""Compute results for pubs that all require the same value of ``shots``."""
# prepare circuits
bound_circuits = [pub.parameter_values.bind_all(pub.circuit) for pub in pubs]
Expand Down Expand Up @@ -197,7 +197,7 @@ def _postprocess_pub(
shape: tuple[int, ...],
meas_info: list[_MeasureInfo],
max_num_bytes: int,
) -> PubResult:
) -> SamplerPubResult:
"""Converts the memory data into an array of bit arrays with the shape of the pub."""
arrays = {
item.creg_name: np.zeros(shape + (shots, item.num_bytes), dtype=np.uint8)
Expand All @@ -213,7 +213,7 @@ def _postprocess_pub(
meas = {
item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info
}
return PubResult(DataBin(**meas, shape=shape), metadata={})
return SamplerPubResult(DataBin(**meas, shape=shape), metadata={})


def _analyze_circuit(circuit: QuantumCircuit) -> tuple[list[_MeasureInfo], int]:
Expand Down
4 changes: 2 additions & 2 deletions qiskit/primitives/base/base_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
from qiskit.providers import JobV1 as Job

from ..containers.primitive_result import PrimitiveResult
from ..containers.pub_result import PubResult
from ..containers.sampler_pub import SamplerPubLike
from ..containers.sampler_pub_result import SamplerPubResult
from . import validation
from .base_primitive import BasePrimitive
from .base_primitive_job import BasePrimitiveJob
Expand Down Expand Up @@ -165,7 +165,7 @@ class BaseSamplerV2(ABC):
@abstractmethod
def run(
self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None
) -> BasePrimitiveJob[PrimitiveResult[PubResult]]:
) -> BasePrimitiveJob[PrimitiveResult[SamplerPubResult]]:
"""Run and collect samples from each pub.
Args:
Expand Down
9 changes: 5 additions & 4 deletions qiskit/primitives/containers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
"""


from .bindings_array import BindingsArrayLike
from .bit_array import BitArray
from .data_bin import make_data_bin, DataBin
from .data_bin import DataBin, make_data_bin
from .estimator_pub import EstimatorPubLike
from .observables_array import ObservableLike, ObservablesArrayLike
from .primitive_result import PrimitiveResult
from .pub_result import PubResult
from .estimator_pub import EstimatorPubLike
from .sampler_pub import SamplerPubLike
from .bindings_array import BindingsArrayLike
from .observables_array import ObservableLike, ObservablesArrayLike
from .sampler_pub_result import SamplerPubResult
2 changes: 1 addition & 1 deletion qiskit/primitives/containers/pub_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# that they have been altered from the originals.

"""
Base Pub class
Base Pub result class
"""

from __future__ import annotations
Expand Down
74 changes: 74 additions & 0 deletions qiskit/primitives/containers/sampler_pub_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Sampler Pub result class
"""

from __future__ import annotations

from typing import Iterable

import numpy as np

from .bit_array import BitArray
from .pub_result import PubResult


class SamplerPubResult(PubResult):
"""Result of Sampler Pub."""

def join_data(self, names: Iterable[str] | None = None) -> BitArray | np.ndarray:
"""Join data from many registers into one data container.
Data is joined along the bits axis. For example, for :class:`~.BitArray` data, this corresponds
to bitstring concatenation.
Args:
names: Which registers to join. Their order is maintained, for example, given
``["alpha", "beta"]``, the data from register ``alpha`` is placed to the left of the
data from register ``beta``. When ``None`` is given, this value is set to the
ordered list of register names, which will have been preserved from the input circuit
order.
Returns:
Joint data.
Raises:
ValueError: If specified names are empty.
ValueError: If specified name does not exist.
TypeError: If specified data comes from incompatible types.
"""
if names is None:
names = list(self.data)
if not names:
raise ValueError("No entry exists in the data bin.")
else:
names = list(names)
if not names:
raise ValueError("An empty name list is given.")
for name in names:
if name not in self.data:
raise ValueError(f"Name '{name}' does not exist.")

data = [self.data[name] for name in names]
if isinstance(data[0], BitArray):
if not all(isinstance(datum, BitArray) for datum in data):
raise TypeError("Data comes from incompatible types.")
joint_data = BitArray.concatenate_bits(data)
elif isinstance(data[0], np.ndarray):
if not all(isinstance(datum, np.ndarray) for datum in data):
raise TypeError("Data comes from incompatible types.")
joint_data = np.concatenate(data, axis=-1)
else:
raise TypeError("Data comes from incompatible types.")
return joint_data
12 changes: 6 additions & 6 deletions qiskit/primitives/statevector_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@

from __future__ import annotations

import warnings
from dataclasses import dataclass
from typing import Iterable
import warnings

import numpy as np
from numpy.typing import NDArray
Expand All @@ -32,7 +32,7 @@
BitArray,
DataBin,
PrimitiveResult,
PubResult,
SamplerPubResult,
SamplerPubLike,
)
from .containers.sampler_pub import SamplerPub
Expand Down Expand Up @@ -154,7 +154,7 @@ def seed(self) -> np.random.Generator | int | None:

def run(
self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None
) -> PrimitiveJob[PrimitiveResult[PubResult]]:
) -> PrimitiveJob[PrimitiveResult[SamplerPubResult]]:
if shots is None:
shots = self._default_shots
coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs]
Expand All @@ -169,11 +169,11 @@ def run(
job._submit()
return job

def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[PubResult]:
def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[SamplerPubResult]:
results = [self._run_pub(pub) for pub in pubs]
return PrimitiveResult(results)

def _run_pub(self, pub: SamplerPub) -> PubResult:
def _run_pub(self, pub: SamplerPub) -> SamplerPubResult:
circuit, qargs, meas_info = _preprocess_circuit(pub.circuit)
bound_circuits = pub.parameter_values.bind_all(circuit)
arrays = {
Expand All @@ -197,7 +197,7 @@ def _run_pub(self, pub: SamplerPub) -> PubResult:
meas = {
item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info
}
return PubResult(DataBin(**meas, shape=pub.shape), metadata={"shots": pub.shots})
return SamplerPubResult(DataBin(**meas, shape=pub.shape), metadata={"shots": pub.shots})


def _preprocess_circuit(circuit: QuantumCircuit):
Expand Down
17 changes: 17 additions & 0 deletions releasenotes/notes/sampler-pub-result-e64e7de1bae2d35e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
features_primitives:
- |
The subclass :class:`~.SamplerPubResult` of :class:`~.PubResult` was added,
which :class:`~.BaseSamplerV2` implementations can return. The main feature
added in this new subclass is :meth:`~.SamplerPubResult.join_data`, which
joins together (a subset of) the contents of :attr:`~.PubResult.data` into
a single object. This enables the following patterns:
.. code:: python
job_result = sampler.run([pub1, pub2, pub3], shots=123).result()
# assuming all returned data entries are BitArrays
counts1 = job_result[0].join_data().get_counts()
bistrings2 = job_result[1].join_data().get_bitstrings()
array3 = job_result[2].join_data().array
104 changes: 104 additions & 0 deletions test/python/primitives/containers/test_sampler_pub_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.


"""Unit tests for SamplerPubResult."""

from test import QiskitTestCase

import numpy as np

from qiskit.primitives.containers import BitArray, DataBin, SamplerPubResult


class SamplerPubResultCase(QiskitTestCase):
"""Test the SamplerPubResult class."""

def test_construction(self):
"""Test that the constructor works."""
ba = BitArray.from_samples(["00", "11"], 2)
counts = {"00": 1, "11": 1}
data_bin = DataBin(a=ba, b=ba)
pub_result = SamplerPubResult(data_bin)
self.assertEqual(pub_result.data.a.get_counts(), counts)
self.assertEqual(pub_result.data.b.get_counts(), counts)
self.assertEqual(pub_result.metadata, {})

pub_result = SamplerPubResult(data_bin, {"x": 1})
self.assertEqual(pub_result.data.a.get_counts(), counts)
self.assertEqual(pub_result.data.b.get_counts(), counts)
self.assertEqual(pub_result.metadata, {"x": 1})

def test_repr(self):
"""Test that the repr doesn't fail"""
# we are primarily interested in making sure some future change doesn't cause the repr to
# raise an error. it is more sensible for humans to detect a deficiency in the formatting
# itself, should one be uncovered
ba = BitArray.from_samples(["00", "11"], 2)
data_bin = DataBin(a=ba, b=ba)
self.assertTrue(repr(SamplerPubResult(data_bin)).startswith("SamplerPubResult"))
self.assertTrue(repr(SamplerPubResult(data_bin, {"x": 1})).startswith("SamplerPubResult"))

def test_join_data_failures(self):
"""Test the join_data() failure mechanisms work."""

result = SamplerPubResult(DataBin())
with self.assertRaisesRegex(ValueError, "No entry exists in the data bin"):
result.join_data()

alpha = BitArray.from_samples(["00", "11"], 2)
beta = BitArray.from_samples(["010", "101"], 3)
result = SamplerPubResult(DataBin(alpha=alpha, beta=beta))
with self.assertRaisesRegex(ValueError, "An empty name list is given"):
result.join_data([])

alpha = BitArray.from_samples(["00", "11"], 2)
beta = BitArray.from_samples(["010", "101"], 3)
result = SamplerPubResult(DataBin(alpha=alpha, beta=beta))
with self.assertRaisesRegex(ValueError, "Name 'foo' does not exist"):
result.join_data(["alpha", "foo"])

alpha = BitArray.from_samples(["00", "11"], 2)
beta = np.empty((2,))
result = SamplerPubResult(DataBin(alpha=alpha, beta=beta))
with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"):
result.join_data()

alpha = np.empty((2,))
beta = BitArray.from_samples(["00", "11"], 2)
result = SamplerPubResult(DataBin(alpha=alpha, beta=beta))
with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"):
result.join_data()

result = SamplerPubResult(DataBin(alpha=1, beta={}))
with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"):
result.join_data()

def test_join_data_bit_array_default(self):
"""Test the join_data() method with no arguments and bit arrays."""
alpha = BitArray.from_samples(["00", "11"], 2)
beta = BitArray.from_samples(["010", "101"], 3)
data_bin = DataBin(alpha=alpha, beta=beta)
result = SamplerPubResult(data_bin)

gamma = result.join_data()
self.assertEqual(list(gamma.get_bitstrings()), ["01000", "10111"])

def test_join_data_ndarray_default(self):
"""Test the join_data() method with no arguments and ndarrays."""
alpha = np.linspace(0, 1, 30).reshape((2, 3, 5))
beta = np.linspace(0, 1, 12).reshape((2, 3, 2))
data_bin = DataBin(alpha=alpha, beta=beta, shape=(2, 3))
result = SamplerPubResult(data_bin)

gamma = result.join_data()
np.testing.assert_allclose(gamma, np.concatenate([alpha, beta], axis=2))

0 comments on commit 849fa00

Please sign in to comment.