Skip to content

Commit

Permalink
Add BaseSamplerV2 (Qiskit#11529)
Browse files Browse the repository at this point in the history
* Add BaseSamplerV2

Taken from 11264. Removes Options and adds `shots` attributes to BaseSamplerV2

Co-Authored-By: Takashi Imamichi <[email protected]>
Co-Authored-By: Ian Hincks <[email protected]>

* linting

* Update interface contract to place shots in pubs and run

* Add missing doc string

* Apply suggestions from code review

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

* Fix comment commit lint errors

* Add SamplerPub unittests

* linting

* Apply suggestions from code review

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

* linting

* Update job return type

---------

Co-authored-by: Takashi Imamichi <[email protected]>
Co-authored-by: Ian Hincks <[email protected]>
Co-authored-by: Ian Hincks <[email protected]>
  • Loading branch information
4 people authored Jan 17, 2024
1 parent d01346f commit c799435
Show file tree
Hide file tree
Showing 7 changed files with 583 additions and 10 deletions.
2 changes: 1 addition & 1 deletion qiskit/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@
from .base import BaseEstimator, BaseSampler
from .base.estimator_result import EstimatorResult
from .base.sampler_result import SamplerResult
from .containers import BindingsArray, ObservablesArray, PrimitiveResult, PubResult
from .containers import BindingsArray, ObservablesArray, PrimitiveResult, PubResult, SamplerPub
from .estimator import Estimator
from .sampler import Sampler
2 changes: 1 addition & 1 deletion qiskit/primitives/backend_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Sampler implementation for an artibtrary Backend object."""
"""Sampler implementation for an arbitrary Backend object."""

from __future__ import annotations

Expand Down
2 changes: 1 addition & 1 deletion qiskit/primitives/base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
"""

from .base_estimator import BaseEstimator
from .base_sampler import BaseSampler
from .base_sampler import BaseSampler, BaseSamplerV2
from .estimator_result import EstimatorResult
from .sampler_result import SamplerResult
107 changes: 100 additions & 7 deletions qiskit/primitives/base/base_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,67 @@
# that they have been altered from the originals.

r"""
===================
Overview of Sampler
===================
=====================
Overview of SamplerV2
=====================
:class:`~BaseSamplerV2` is a primitive that samples outputs of quantum circuits.
Following construction, a sampler is used by calling its :meth:`~.BaseSamplerV2.run` method
with a list of pubs (Primitive Unified Blocks). Each pub contains values that, together,
define a computational unit of work for the sampler to complete:
* A single :class:`~qiskit.circuit.QuantumCircuit`, possibly parameterized.
* A collection parameter value sets to bind the circuit against if it is parametric.
* Optionally, the number of shots to sample, determined in the run method if not set.
Running a sampler returns a :class:`~qiskit.provider.JobV1` object, where calling
the method :meth:`~qiskit.provider.JobV1.result` results in output samples and metadata
for each pub.
Here is an example of how a sampler is used.
.. code-block:: python
from qiskit.primitives.statevector_sampler import Sampler
from qiskit import QuantumCircuit
from qiskit.circuit.library import RealAmplitudes
# create a Bell circuit
bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
bell.measure_all()
# create two parameterized circuits
pqc = RealAmplitudes(num_qubits=2, reps=2)
pqc.measure_all()
pqc2 = RealAmplitudes(num_qubits=2, reps=3)
pqc2.measure_all()
theta1 = [0, 1, 1, 2, 3, 5]
theta2 = [0, 1, 2, 3, 4, 5, 6, 7]
# initialization of the sampler
sampler = Sampler()
# collect 128 shots from the Bell circuit
job = sampler.run([bell], shots=128)
job_result = job.result()
print(f"The primitive-job finished with result {job_result}"))
# run a sampler job on the parameterized circuits
job2 = sampler.run([(pqc, theta1), (pqc2, theta2)]
job_result = job2.result()
print(f"The primitive-job finished with result {job_result}"))
=====================
Overview of SamplerV1
=====================
Sampler class calculates probabilities or quasi-probabilities of bitstrings from quantum circuits.
Expand Down Expand Up @@ -77,22 +135,26 @@

import warnings
from abc import abstractmethod
from collections.abc import Sequence
from collections.abc import Iterable, Sequence
from copy import copy
from typing import Generic, TypeVar

from qiskit.utils.deprecation import deprecate_func
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.parametertable import ParameterView
from qiskit.providers import JobV1 as Job
from qiskit.utils.deprecation import deprecate_func

from .base_primitive import BasePrimitive
from ..containers.primitive_result import PrimitiveResult
from ..containers.pub_result import PubResult
from ..containers.sampler_pub import SamplerPubLike
from . import validation
from .base_primitive import BasePrimitive
from .base_primitive_job import BasePrimitiveJob

T = TypeVar("T", bound=Job)


class BaseSampler(BasePrimitive, Generic[T]):
class BaseSamplerV1(BasePrimitive, Generic[T]):
"""Sampler base class
Base class of Sampler that calculates quasi-probabilities of bitstrings from quantum circuits.
Expand Down Expand Up @@ -200,3 +262,34 @@ def parameters(self) -> tuple[ParameterView, ...]:
List of the parameters in each quantum circuit.
"""
return tuple(self._parameters)


BaseSampler = BaseSamplerV1


class BaseSamplerV2:
"""Sampler base class version 2.
A Sampler returns samples of quantum circuit outputs.
All sampler implementations must implement default value for the ``shots`` in the
:meth:`.run` method if ``None`` is given both as a ``kwarg`` and in all of the pubs.
"""

@abstractmethod
def run(
self, pubs: Iterable[SamplerPubLike], shots: int | None = None
) -> BasePrimitiveJob[PrimitiveResult[PubResult]]:
"""Run and collect samples from each pub.
Args:
pubs: An iterable of pub-like objects. For example, a list of circuits
or tuples ``(circuit, parameter_values)``.
shots: The total number of shots to sample for each :class:`.SamplerPub`.
that does not specify its own shots. If ``None``, the primitive's
default shots value will be used, which can vary by implementation.
Returns:
The job object of Sampler's result.
"""
pass
1 change: 1 addition & 0 deletions qiskit/primitives/containers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
from .observables_array import ObservablesArray
from .primitive_result import PrimitiveResult
from .pub_result import PubResult
from .sampler_pub import SamplerPub, SamplerPubLike
150 changes: 150 additions & 0 deletions qiskit/primitives/containers/sampler_pub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# 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 class
"""

from __future__ import annotations

from typing import Tuple, Union
from numbers import Integral

from qiskit import QuantumCircuit

from .bindings_array import BindingsArray, BindingsArrayLike
from .shape import ShapedMixin


class SamplerPub(ShapedMixin):
"""Pub (Primitive Unified Bloc) for Sampler.
Pub is composed of tuple (circuit, parameter_values, shots).
If shots are provided this number of shots will be run with the sampler,
if ``shots=None`` the number of run shots is determined by the sampler.
"""

def __init__(
self,
circuit: QuantumCircuit,
parameter_values: BindingsArray | None = None,
shots: int | None = None,
validate: bool = True,
):
"""Initialize a sampler pub.
Args:
circuit: A quantum circuit.
parameter_values: A bindings array.
shots: A specific number of shots to run with. This value takes
precedence over any value owed by or supplied to a sampler.
validate: If ``True``, the input data is validated during initialization.
"""
super().__init__()
self._circuit = circuit
self._parameter_values = parameter_values or BindingsArray()
self._shots = shots
self._shape = self._parameter_values.shape
if validate:
self.validate()

@property
def circuit(self) -> QuantumCircuit:
"""A quantum circuit."""
return self._circuit

@property
def parameter_values(self) -> BindingsArray:
"""A bindings array."""
return self._parameter_values

@property
def shots(self) -> int | None:
"""An specific number of shots to run with (optional).
This value takes precedence over any value owed by or supplied to a sampler.
"""
return self._shots

@classmethod
def coerce(cls, pub: SamplerPubLike, shots: int | None = None) -> SamplerPub:
"""Coerce a :class:`~.SamplerPubLike` object into a :class:`~.SamplerPub` instance.
Args:
pub: An object to coerce.
shots: An optional default number of shots to use if not
already specified by the pub-like object.
Returns:
A coerced sampler pub.
"""
# Validate shots kwarg if provided
if shots is not None:
if not isinstance(shots, Integral) or isinstance(shots, bool):
raise TypeError("shots must be an integer")
if shots < 0:
raise ValueError("shots must be non-negative")

if isinstance(pub, SamplerPub):
if pub.shots is None and shots is not None:
return cls(
circuit=pub.circuit,
parameter_values=pub.parameter_values,
shots=shots,
validate=False, # Assume Pub is already validated
)
return pub

if isinstance(pub, QuantumCircuit):
return cls(circuit=pub, shots=shots, validate=True)

if len(pub) not in [1, 2, 3]:
raise ValueError(
f"The length of pub must be 1, 2 or 3, but length {len(pub)} is given."
)
circuit = pub[0]
parameter_values = BindingsArray.coerce(pub[1]) if len(pub) > 1 else None
if len(pub) > 2 and pub[2] is not None:
shots = pub[2]
return cls(circuit=circuit, parameter_values=parameter_values, shots=shots, validate=True)

def validate(self):
"""Validate the pub."""
if not isinstance(self.circuit, QuantumCircuit):
raise TypeError("circuit must be QuantumCircuit.")

self.parameter_values.validate()

if self.shots is not None:
if not isinstance(self.shots, Integral) or isinstance(self.shots, bool):
raise TypeError("shots must be an integer")
if self.shots < 0:
raise ValueError("shots must be non-negative")

# Cross validate circuits and parameter values
num_parameters = self.parameter_values.num_parameters
if num_parameters != self.circuit.num_parameters:
raise ValueError(
f"The number of values ({num_parameters}) does not match "
f"the number of parameters ({self.circuit.num_parameters}) for the circuit."
)


SamplerPubLike = Union[
SamplerPub,
QuantumCircuit,
Tuple[QuantumCircuit],
Tuple[QuantumCircuit, BindingsArrayLike],
Tuple[QuantumCircuit, BindingsArrayLike, Union[Integral, None]],
]
Loading

0 comments on commit c799435

Please sign in to comment.