-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Add BackendSamplerV2 #11928
Merged
Merged
Add BackendSamplerV2 #11928
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
42a753d
add BackendSamplerV2
t-imamichi ee5bb77
reno
t-imamichi c704cf1
Apply suggestions from code review
t-imamichi 6188362
allow BackendV1
t-imamichi 45dc9ad
Merge branch 'main' into backend-sampler-v2
t-imamichi d3bcd74
update docstring
t-imamichi e143049
add options
t-imamichi 14efae7
Merge branch 'main' into backend-sampler-v2
t-imamichi 4e3ca29
move default_shots to options and update doc/reno
t-imamichi 47afe23
Merge branch 'main' into backend-sampler-v2
t-imamichi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
# 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 V2 implementation for an arbitrary Backend object.""" | ||
|
||
from __future__ import annotations | ||
|
||
import warnings | ||
from dataclasses import dataclass | ||
from typing import Iterable | ||
|
||
import numpy as np | ||
from numpy.typing import NDArray | ||
|
||
from qiskit.circuit import QuantumCircuit | ||
from qiskit.primitives.backend_estimator import _run_circuits | ||
from qiskit.primitives.base import BaseSamplerV2 | ||
from qiskit.primitives.containers import ( | ||
BitArray, | ||
PrimitiveResult, | ||
PubResult, | ||
SamplerPubLike, | ||
make_data_bin, | ||
) | ||
from qiskit.primitives.containers.bit_array import _min_num_bytes | ||
from qiskit.primitives.containers.sampler_pub import SamplerPub | ||
from qiskit.primitives.primitive_job import PrimitiveJob | ||
from qiskit.providers.backend import BackendV1, BackendV2 | ||
from qiskit.result import Result | ||
|
||
|
||
@dataclass | ||
class Options: | ||
"""Options for :class:`~.BackendSamplerV2`""" | ||
|
||
default_shots: int = 1024 | ||
"""The default shots to use if none are specified in :meth:`~.run`. | ||
Default: 1024. | ||
""" | ||
|
||
seed_simulator: int | None = None | ||
"""The seed to use in the simulator. If None, a random seed will be used. | ||
Default: None. | ||
""" | ||
|
||
|
||
@dataclass | ||
class _MeasureInfo: | ||
creg_name: str | ||
num_bits: int | ||
num_bytes: int | ||
start: int | ||
|
||
|
||
class BackendSamplerV2(BaseSamplerV2): | ||
"""Evaluates bitstrings for provided quantum circuits | ||
|
||
The :class:`~.BackendSamplerV2` class is a generic implementation of the | ||
:class:`~.BaseSamplerV2` interface that is used to wrap a :class:`~.BackendV2` | ||
(or :class:`~.BackendV1`) object in the class :class:`~.BaseSamplerV2` API. It | ||
facilitates using backends that do not provide a native | ||
:class:`~.BaseSamplerV2` implementation in places that work with | ||
:class:`~.BaseSamplerV2`. However, | ||
if you're using a provider that has a native implementation of | ||
:class:`~.BaseSamplerV2`, it is a better choice to leverage that native | ||
implementation as it will likely include additional optimizations and be | ||
a more efficient implementation. The generic nature of this class | ||
precludes doing any provider- or backend-specific optimizations. | ||
|
||
This class does not perform any measurement or gate mitigation. | ||
|
||
Each tuple of ``(circuit, <optional> parameter values, <optional> shots)``, called a sampler | ||
primitive unified bloc (PUB), produces its own array-valued result. The :meth:`~run` method can | ||
be given many pubs at once. | ||
|
||
The options for :class:`~.BackendSamplerV2` consist of the following items. | ||
|
||
* ``default_shots``: The default shots to use if none are specified in :meth:`~run`. | ||
Default: 1024. | ||
|
||
* ``seed_simulator``: The seed to use in the simulator. If None, a random seed will be used. | ||
Default: None. | ||
|
||
.. note:: | ||
|
||
This class requires a backend that supports ``memory`` option. | ||
|
||
""" | ||
|
||
def __init__( | ||
self, | ||
*, | ||
backend: BackendV1 | BackendV2, | ||
options: dict | None = None, | ||
): | ||
""" | ||
Args: | ||
backend: The backend to run the primitive on. | ||
options: The options to control the default shots (``default_shots``) and | ||
the random seed for the simulator (``seed_simulator``). | ||
""" | ||
self._backend = backend | ||
self._options = Options(**options) if options else Options() | ||
|
||
@property | ||
def backend(self) -> BackendV1 | BackendV2: | ||
"""Returns the backend which this sampler object based on.""" | ||
return self._backend | ||
|
||
@property | ||
def options(self) -> Options: | ||
"""Return the options""" | ||
return self._options | ||
|
||
def run( | ||
self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None | ||
) -> PrimitiveJob[PrimitiveResult[PubResult]]: | ||
if shots is None: | ||
shots = self._options.default_shots | ||
coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs] | ||
self._validate_pubs(coerced_pubs) | ||
job = PrimitiveJob(self._run, coerced_pubs) | ||
job._submit() | ||
return job | ||
|
||
def _validate_pubs(self, pubs: list[SamplerPub]): | ||
for i, pub in enumerate(pubs): | ||
if len(pub.circuit.cregs) == 0: | ||
warnings.warn( | ||
f"The {i}-th pub's circuit has no output classical registers and so the result " | ||
"will be empty. Did you mean to add measurement instructions?", | ||
UserWarning, | ||
) | ||
|
||
def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[PubResult]: | ||
results = [self._run_pub(pub) for pub in pubs] | ||
return PrimitiveResult(results) | ||
|
||
def _run_pub(self, pub: SamplerPub) -> PubResult: | ||
meas_info, max_num_bytes = _analyze_circuit(pub.circuit) | ||
bound_circuits = pub.parameter_values.bind_all(pub.circuit) | ||
arrays = { | ||
item.creg_name: np.zeros( | ||
bound_circuits.shape + (pub.shots, item.num_bytes), dtype=np.uint8 | ||
) | ||
for item in meas_info | ||
} | ||
flatten_circuits = np.ravel(bound_circuits).tolist() | ||
result_memory, _ = _run_circuits( | ||
flatten_circuits, | ||
self._backend, | ||
memory=True, | ||
shots=pub.shots, | ||
seed_simulator=self._options.seed_simulator, | ||
) | ||
memory_list = _prepare_memory(result_memory, max_num_bytes) | ||
|
||
for samples, index in zip(memory_list, np.ndindex(*bound_circuits.shape)): | ||
for item in meas_info: | ||
ary = _samples_to_packed_array(samples, item.num_bits, item.start) | ||
arrays[item.creg_name][index] = ary | ||
|
||
data_bin_cls = make_data_bin( | ||
[(item.creg_name, BitArray) for item in meas_info], | ||
shape=bound_circuits.shape, | ||
) | ||
meas = { | ||
item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info | ||
} | ||
data_bin = data_bin_cls(**meas) | ||
return PubResult(data_bin, metadata={}) | ||
|
||
|
||
def _analyze_circuit(circuit: QuantumCircuit) -> tuple[list[_MeasureInfo], int]: | ||
meas_info = [] | ||
max_num_bits = 0 | ||
for creg in circuit.cregs: | ||
name = creg.name | ||
num_bits = creg.size | ||
start = circuit.find_bit(creg[0]).index | ||
meas_info.append( | ||
_MeasureInfo( | ||
creg_name=name, | ||
num_bits=num_bits, | ||
num_bytes=_min_num_bytes(num_bits), | ||
start=start, | ||
) | ||
) | ||
max_num_bits = max(max_num_bits, start + num_bits) | ||
return meas_info, _min_num_bytes(max_num_bits) | ||
|
||
|
||
def _prepare_memory(results: list[Result], num_bytes: int) -> NDArray[np.uint8]: | ||
lst = [] | ||
for res in results: | ||
for exp in res.results: | ||
if hasattr(exp.data, "memory") and exp.data.memory: | ||
data = b"".join(int(i, 16).to_bytes(num_bytes, "big") for i in exp.data.memory) | ||
data = np.frombuffer(data, dtype=np.uint8).reshape(-1, num_bytes) | ||
else: | ||
# no measure in a circuit | ||
data = np.zeros((exp.shots, num_bytes), dtype=np.uint8) | ||
lst.append(data) | ||
ary = np.array(lst, copy=False) | ||
return np.unpackbits(ary, axis=-1, bitorder="big") | ||
|
||
|
||
def _samples_to_packed_array( | ||
samples: NDArray[np.uint8], num_bits: int, start: int | ||
) -> NDArray[np.uint8]: | ||
# samples of `Backend.run(memory=True)` will be the order of | ||
# clbit_last, ..., clbit_1, clbit_0 | ||
# place samples in the order of clbit_start+num_bits-1, ..., clbit_start+1, clbit_start | ||
if start == 0: | ||
ary = samples[:, -start - num_bits :] | ||
else: | ||
ary = samples[:, -start - num_bits : -start] | ||
# pad 0 in the left to align the number to be mod 8 | ||
# since np.packbits(bitorder='big') pads 0 to the right. | ||
pad_size = -num_bits % 8 | ||
ary = np.pad(ary, ((0, 0), (pad_size, 0)), constant_values=0) | ||
# pack bits in big endian order | ||
ary = np.packbits(ary, axis=-1, bitorder="big") | ||
return ary |
28 changes: 28 additions & 0 deletions
28
releasenotes/notes/add-backend-sampler-v2-5e40135781eebc7f.yaml
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe an example with |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
--- | ||
features: | ||
- | | ||
The implementation :class:`~.BackendSamplerV2` of :class:`~.BaseSamplerV2` was added. | ||
This sampler supports :class:`~.BackendV1` and :class:`~.BackendV2` that allow | ||
``memory`` option to compute bitstrings. | ||
|
||
.. code-block:: python | ||
|
||
import numpy as np | ||
from qiskit import transpile | ||
from qiskit.circuit.library import IQP | ||
from qiskit.primitives import BackendSamplerV2 | ||
from qiskit.providers.fake_provider import Fake7QPulseV1 | ||
from qiskit.quantum_info import random_hermitian | ||
|
||
backend = Fake7QPulseV1() | ||
sampler = BackendSamplerV2(backend=backend) | ||
n_qubits = 5 | ||
mat = np.real(random_hermitian(n_qubits, seed=1234)) | ||
circuit = IQP(mat) | ||
circuit.measure_all() | ||
isa_circuit = transpile(circuit, backend=backend, optimization_level=1) | ||
job = sampler.run([isa_circuit], shots=100) | ||
result = job.result() | ||
print(f"> bitstrings: {result[0].data.meas.get_bitstrings()}") | ||
print(f"> counts: {result[0].data.meas.get_counts()}") | ||
print(f"> Metadata: {result[0].metadata}") |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to check for measure instruction? Should we rather check measure instruction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure whether we need more validations. This is basically same as the check of
StatevectorSampler
.qiskit/qiskit/primitives/statevector_sampler.py
Lines 161 to 166 in ff2bbfe