Skip to content
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 BaseSamplerV2 #11529

Merged
merged 11 commits into from
Jan 17, 2024
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
ihincks marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member Author

Choose a reason for hiding this comment

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

@jakelishman Is this the sort of thing we should have a from __future__ import <what?> so that BaseSampler gets set to BaseSamplerV2 instead? And that if you import BaseSampler without this import it raises a deprecation warning of some sort using the same trick you used in the other PRs and returns BaseSamplerV1 for it in those cases?

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess for 1.0 we might want a future deprecation warning rather than actual deprecation warning for the V1 primitives, and then the actual warning on 1.1?



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`.
Copy link
Member

Choose a reason for hiding this comment

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

If SamplerV2 has an execution option and the number of shots of the execution option and that of run method differ, I guess that of run will be used. Am I correct?

E.g.,

sampler = SamplerV2(options={"execution": {"shots": 10}})
result = sampler.run([circuit], shots=20).result()
# `result` contains 20 samples instead of 10 samples.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, in this situation the kwargs of run takes precedence over execution.shots. The latter should be thought of as the default value when run gets None.

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):
chriseclectic marked this conversation as resolved.
Show resolved Hide resolved
"""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,
ihincks marked this conversation as resolved.
Show resolved Hide resolved
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.
"""
chriseclectic marked this conversation as resolved.
Show resolved Hide resolved
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:
Copy link
Member

@t-imamichi t-imamichi Jan 11, 2024

Choose a reason for hiding this comment

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

I guess shots of this method is used as a default value not to overwrite the existing pub.shots.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's right, pub.shots takes precedence.

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