From 5a850988e824eeeee542452c7af40d0afff71a46 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 9 Jan 2024 12:42:38 -0500 Subject: [PATCH 01/11] Add BaseSamplerV2 Taken from 11264. Removes Options and adds `shots` attributes to BaseSamplerV2 Co-Authored-By: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Co-Authored-By: Ian Hincks <2229105+ihincks@users.noreply.github.com> --- qiskit/primitives/__init__.py | 2 +- qiskit/primitives/backend_sampler.py | 2 +- qiskit/primitives/base/__init__.py | 2 +- qiskit/primitives/base/base_sampler.py | 109 ++++++++++++++++++-- qiskit/primitives/containers/__init__.py | 1 + qiskit/primitives/containers/sampler_pub.py | 100 ++++++++++++++++++ 6 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 qiskit/primitives/containers/sampler_pub.py diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index ecc24757f815..2f54a3346d44 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -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 diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index 1f588921cc79..63f4626eb819 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -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 diff --git a/qiskit/primitives/base/__init__.py b/qiskit/primitives/base/__init__.py index d7695fbf4259..1b537b0dc0ef 100644 --- a/qiskit/primitives/base/__init__.py +++ b/qiskit/primitives/base/__init__.py @@ -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 diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index d21487261091..491f9740340a 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -11,9 +11,66 @@ # that they have been altered from the originals. r""" -=================== -Overview of Sampler -=================== +===================== +Overview of SamplerV2 +===================== + +:class:`~BaseSamplerV2` is a primitive that samples bitstrings from 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 two values that, together, +define a computation unit of work for the sampler to complete: + +* a single :class:`~qiskit.circuit.QuantumCircuit`, possibly parameterized, whose final state we + define as :math:`\psi(\theta)`, + +* a collection parameter value sets to bind the circuit against, :math:`\theta_k`. + +Running a sampler returns a :class:`~qiskit.provider.JobV1 object, where calling +the method :meth:`~qiskit.provider.JobV1.result` results in bitstring samples and metadata +for each pub. + +Here is an example of how sampler is used. + + +.. code-block:: python + + from qiskit.primitives.statevector_sampler import Sampler + from qiskit import QuantumCircuit + from qiskit.circuit.library import RealAmplitudes + + # a Bell circuit + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + bell.measure_all() + + # 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() + + # Sampler runs a job on the Bell circuit + job = sampler.run([bell]) + job_result = job.result() + print(f"The primitive-job finished with result {job_result}")) + + # Sampler runs a 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. @@ -77,22 +134,23 @@ 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 typing import Generic, Optional, 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.sampler_pub import SamplerPubLike from . import validation +from .base_primitive import BasePrimitive 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. @@ -200,3 +258,38 @@ 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 bitstrings of quantum circuits. + """ + + def __init__(self, shots: int | None): + self._shots = shots + + @property + def shots(self) -> int | None: + """The target number of total shots for each run :class:`.SamplerPub`.""" + return self._shots + + @shots.setter + def shots(self, value: int | None): + self._shots = value + + @abstractmethod + def run(self, pubs: Iterable[SamplerPubLike]) -> Job: + """Run the pubs of samples. + + Args: + pubs: an iterable of pub-like object. Typically, list of tuple + ``(QuantumCircuit, parameter_values)`` + + Returns: + The job object of Sampler's Result. + """ + pass diff --git a/qiskit/primitives/containers/__init__.py b/qiskit/primitives/containers/__init__.py index 2c9d614a71aa..0896f4b6bc78 100644 --- a/qiskit/primitives/containers/__init__.py +++ b/qiskit/primitives/containers/__init__.py @@ -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 diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py new file mode 100644 index 000000000000..998394db1e35 --- /dev/null +++ b/qiskit/primitives/containers/sampler_pub.py @@ -0,0 +1,100 @@ +# 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 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 double (circuit, parameter_values). + """ + + __slots__ = ("_circuit", "_parameter_values") + + def __init__( + self, + circuit: QuantumCircuit, + parameter_values: BindingsArray | None = None, + validate: bool = False, + ): + """Initialize a sampler pub. + + Args: + circuit: a quantum circuit. + parameter_values: a bindings array. + validate: if True, the input data is validated during initialization. + """ + self._circuit = circuit + self._parameter_values = parameter_values or BindingsArray() + self._shape = self._parameter_values.shape + if validate: + self.validate() + + @property + def parameter_values(self) -> BindingsArray: + """A bindings array""" + return self._parameter_values + + @classmethod + def coerce(cls, pub: SamplerPubLike) -> SamplerPub: + """Coerce SamplerPubLike into SamplerPub. + + Args: + pub: an object to be Sampler pub. + + Returns: + A coerced sampler pub. + """ + if isinstance(pub, SamplerPub): + return pub + if isinstance(pub, QuantumCircuit): + return cls(circuit=pub) + if len(pub) not in [1, 2]: + raise ValueError(f"The length of pub must be 1 or 2, but length {len(pub)} is given.") + circuit = pub[0] + if len(pub) == 1: + return cls(circuit=circuit) + parameter_values = BindingsArray.coerce(pub[1]) + return cls(circuit=circuit, parameter_values=parameter_values) + + def validate(self): + """Validate the pub.""" + if not isinstance(self.circuit, QuantumCircuit): + raise TypeError("circuit must be QuantumCircuit.") + + self.parameter_values.validate() + + # 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] +] From 7bfc0232821aafd6231989b3d9e757614ff31606 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 10 Jan 2024 10:32:26 -0500 Subject: [PATCH 02/11] linting --- qiskit/primitives/base/base_sampler.py | 2 +- qiskit/primitives/containers/sampler_pub.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 491f9740340a..d507045768d0 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -136,7 +136,7 @@ from abc import abstractmethod from collections.abc import Iterable, Sequence from copy import copy -from typing import Generic, Optional, TypeVar +from typing import Generic, TypeVar from qiskit.circuit import QuantumCircuit from qiskit.circuit.parametertable import ParameterView diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py index 998394db1e35..eea7f7978b0c 100644 --- a/qiskit/primitives/containers/sampler_pub.py +++ b/qiskit/primitives/containers/sampler_pub.py @@ -46,6 +46,7 @@ def __init__( parameter_values: a bindings array. validate: if True, the input data is validated during initialization. """ + super().__init__() self._circuit = circuit self._parameter_values = parameter_values or BindingsArray() self._shape = self._parameter_values.shape From 765fb0aac1bcc1a57f1f4e237de6d2b607ebd989 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 10 Jan 2024 11:51:36 -0500 Subject: [PATCH 03/11] Update interface contract to place shots in pubs and run --- qiskit/primitives/base/base_sampler.py | 23 +++----- qiskit/primitives/containers/sampler_pub.py | 61 +++++++++++++++++---- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index d507045768d0..81abe4156702 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -267,27 +267,22 @@ class BaseSamplerV2: """Sampler base class version 2. A Sampler returns samples of bitstrings of quantum circuits. - """ - - def __init__(self, shots: int | None): - self._shots = shots - @property - def shots(self) -> int | None: - """The target number of total shots for each run :class:`.SamplerPub`.""" - return self._shots - - @shots.setter - def shots(self, value: int | None): - self._shots = value + A Sampler implementation must treat the :meth:`.run` method ``shots=None`` kwarg + as using a default ``shots`` value. The default value and methods to set it can + be determined by the Sampler implementor. + """ @abstractmethod - def run(self, pubs: Iterable[SamplerPubLike]) -> Job: + def run(self, pubs: Iterable[SamplerPubLike], shots: int | None = None) -> Job: """Run the pubs of samples. Args: pubs: an iterable of pub-like object. Typically, list of tuple - ``(QuantumCircuit, parameter_values)`` + ``(QuantumCircuit, parameter_values)`` + shots: the total number of shots for each run :class:`.SamplerPub`. + that does not specify its own shots. If None the primitives + default shots value will be used. Returns: The job object of Sampler's Result. diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py index eea7f7978b0c..9dc58a0a1291 100644 --- a/qiskit/primitives/containers/sampler_pub.py +++ b/qiskit/primitives/containers/sampler_pub.py @@ -18,6 +18,7 @@ from __future__ import annotations from typing import Tuple, Union +from numbers import Integral from qiskit import QuantumCircuit @@ -28,15 +29,19 @@ class SamplerPub(ShapedMixin): """Pub (Primitive Unified Bloc) for Sampler. - Pub is composed of double (circuit, parameter_values). + 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. """ - __slots__ = ("_circuit", "_parameter_values") + __slots__ = ("_circuit", "_parameter_values", "_shots") def __init__( self, circuit: QuantumCircuit, parameter_values: BindingsArray | None = None, + shots: int | None = None, validate: bool = False, ): """Initialize a sampler pub. @@ -49,36 +54,58 @@ def __init__( 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""" + """A bindings array.""" return self._parameter_values + @property + def shots(self) -> int | None: + """An specific number of shots to run with (optional).""" + return self._shots + @classmethod - def coerce(cls, pub: SamplerPubLike) -> SamplerPub: + def coerce(cls, pub: SamplerPubLike, shots: int | None = None) -> SamplerPub: """Coerce SamplerPubLike into SamplerPub. Args: pub: an object to be Sampler pub. + shots: an optional default number of shots to use if not + already specified by the pub-like object. Returns: A coerced sampler pub. """ if isinstance(pub, SamplerPub): + if pub.shots is None and shots is not None: + return cls( + pub.circuit, + pub.parameter_values, + shots=shots, + validate=False, + ) return pub if isinstance(pub, QuantumCircuit): - return cls(circuit=pub) - if len(pub) not in [1, 2]: - raise ValueError(f"The length of pub must be 1 or 2, but length {len(pub)} is given.") + return cls(circuit=pub, shots=shots) + 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] - if len(pub) == 1: - return cls(circuit=circuit) - parameter_values = BindingsArray.coerce(pub[1]) - return cls(circuit=circuit, parameter_values=parameter_values) + 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=False) def validate(self): """Validate the pub.""" @@ -87,6 +114,12 @@ def validate(self): self.parameter_values.validate() + if self.shots is not None: + if not isinstance(self.shots, Integral): + 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: @@ -97,5 +130,9 @@ def validate(self): SamplerPubLike = Union[ - SamplerPub, QuantumCircuit, Tuple[QuantumCircuit], Tuple[QuantumCircuit, BindingsArrayLike] + SamplerPub, + QuantumCircuit, + Tuple[QuantumCircuit], + Tuple[QuantumCircuit, BindingsArrayLike], + Tuple[QuantumCircuit, BindingsArrayLike, Integral], ] From 9db4a48cc7714a86099ced942b1d11abea170442 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 10 Jan 2024 12:19:50 -0500 Subject: [PATCH 04/11] Add missing doc string --- qiskit/primitives/containers/sampler_pub.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py index 9dc58a0a1291..6e8cfb051e4c 100644 --- a/qiskit/primitives/containers/sampler_pub.py +++ b/qiskit/primitives/containers/sampler_pub.py @@ -49,6 +49,7 @@ def __init__( Args: circuit: a quantum circuit. parameter_values: a bindings array. + shots: A specific number of shots to run with. validate: if True, the input data is validated during initialization. """ super().__init__() From e9fd26f2da4b03ee0429d187ba535a2297fc2703 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 10 Jan 2024 14:09:23 -0500 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Ian Hincks --- qiskit/primitives/base/base_sampler.py | 39 ++++++++++----------- qiskit/primitives/containers/sampler_pub.py | 12 +++---- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 81abe4156702..28ded67e4e1b 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -15,22 +15,21 @@ Overview of SamplerV2 ===================== -:class:`~BaseSamplerV2` is a primitive that samples bitstrings from quantum circuits. +: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 two values that, together, -define a computation unit of work for the sampler to complete: +define a computational unit of work for the sampler to complete: -* a single :class:`~qiskit.circuit.QuantumCircuit`, possibly parameterized, whose final state we - define as :math:`\psi(\theta)`, +* A single :class:`~qiskit.circuit.QuantumCircuit`, possibly parameterized. -* a collection parameter value sets to bind the circuit against, :math:`\theta_k`. +* A collection parameter value sets to bind the circuit against if it is parametric. -Running a sampler returns a :class:`~qiskit.provider.JobV1 object, where calling -the method :meth:`~qiskit.provider.JobV1.result` results in bitstring samples and metadata +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 sampler is used. +Here is an example of how a sampler is used. .. code-block:: python @@ -39,13 +38,13 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes - # a Bell circuit + # create a Bell circuit bell = QuantumCircuit(2) bell.h(0) bell.cx(0, 1) bell.measure_all() - # two parameterized circuits + # create two parameterized circuits pqc = RealAmplitudes(num_qubits=2, reps=2) pqc.measure_all() pqc2 = RealAmplitudes(num_qubits=2, reps=3) @@ -57,12 +56,12 @@ # initialization of the sampler sampler = Sampler() - # Sampler runs a job on the Bell circuit + # run a sampler job on the Bell circuit job = sampler.run([bell]) job_result = job.result() print(f"The primitive-job finished with result {job_result}")) - # Sampler runs a job on the parameterized circuits + # 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}")) @@ -266,7 +265,7 @@ def parameters(self) -> tuple[ParameterView, ...]: class BaseSamplerV2: """Sampler base class version 2. - A Sampler returns samples of bitstrings of quantum circuits. + A Sampler returns samples of quantum circuit outputs. A Sampler implementation must treat the :meth:`.run` method ``shots=None`` kwarg as using a default ``shots`` value. The default value and methods to set it can @@ -275,16 +274,16 @@ class BaseSamplerV2: @abstractmethod def run(self, pubs: Iterable[SamplerPubLike], shots: int | None = None) -> Job: - """Run the pubs of samples. + """Run and collect samples from each pub. Args: - pubs: an iterable of pub-like object. Typically, list of tuple - ``(QuantumCircuit, parameter_values)`` - shots: the total number of shots for each run :class:`.SamplerPub`. - that does not specify its own shots. If None the primitives - default shots value will be used. + 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. + The job object of Sampler's result. """ pass diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py index 6e8cfb051e4c..ea383e444c3f 100644 --- a/qiskit/primitives/containers/sampler_pub.py +++ b/qiskit/primitives/containers/sampler_pub.py @@ -47,10 +47,10 @@ def __init__( """Initialize a sampler pub. Args: - circuit: a quantum circuit. - parameter_values: a bindings array. + circuit: A quantum circuit. + parameter_values: A bindings array. shots: A specific number of shots to run with. - validate: if True, the input data is validated during initialization. + validate: If True, the input data is validated during initialization. """ super().__init__() self._circuit = circuit @@ -77,11 +77,11 @@ def shots(self) -> int | None: @classmethod def coerce(cls, pub: SamplerPubLike, shots: int | None = None) -> SamplerPub: - """Coerce SamplerPubLike into SamplerPub. + """Coerce a :class:`~.SamplerPubLike` object into a :class:`~.SamplerPub` instance. Args: - pub: an object to be Sampler pub. - shots: an optional default number of shots to use if not + pub: An object to coerce. + shots: An optional default number of shots to use if not already specified by the pub-like object. Returns: From 215c637484464fc1b8576c54cfad0cf569826865 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 10 Jan 2024 14:21:11 -0500 Subject: [PATCH 06/11] Fix comment commit lint errors --- qiskit/primitives/base/base_sampler.py | 2 +- qiskit/primitives/containers/sampler_pub.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 28ded67e4e1b..2bd34c9acfc2 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -277,7 +277,7 @@ def run(self, pubs: Iterable[SamplerPubLike], shots: int | None = None) -> Job: """Run and collect samples from each pub. Args: - pubs: An iterable of pub-like objects. For example, a list of circuits + 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 diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py index ea383e444c3f..eafba84e3c67 100644 --- a/qiskit/primitives/containers/sampler_pub.py +++ b/qiskit/primitives/containers/sampler_pub.py @@ -135,5 +135,5 @@ def validate(self): QuantumCircuit, Tuple[QuantumCircuit], Tuple[QuantumCircuit, BindingsArrayLike], - Tuple[QuantumCircuit, BindingsArrayLike, Integral], + Tuple[QuantumCircuit, BindingsArrayLike, Union[Integral, None]], ] From ffae216012ed25914e5da342af7d47f4c099272a Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 10 Jan 2024 16:09:52 -0500 Subject: [PATCH 07/11] Add SamplerPub unittests --- qiskit/primitives/containers/sampler_pub.py | 23 +- .../primitives/containers/test_sampler_pub.py | 337 ++++++++++++++++++ 2 files changed, 353 insertions(+), 7 deletions(-) create mode 100644 test/python/primitives/containers/test_sampler_pub.py diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py index eafba84e3c67..c60b1f7689b8 100644 --- a/qiskit/primitives/containers/sampler_pub.py +++ b/qiskit/primitives/containers/sampler_pub.py @@ -42,7 +42,7 @@ def __init__( circuit: QuantumCircuit, parameter_values: BindingsArray | None = None, shots: int | None = None, - validate: bool = False, + validate: bool = True, ): """Initialize a sampler pub. @@ -87,17 +87,26 @@ def coerce(cls, pub: SamplerPubLike, shots: int | None = None) -> SamplerPub: 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( - pub.circuit, - pub.parameter_values, + circuit=pub.circuit, + parameter_values=pub.parameter_values, shots=shots, - validate=False, + validate=False, # Assume Pub is already validated ) return pub + if isinstance(pub, QuantumCircuit): - return cls(circuit=pub, shots=shots) + 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." @@ -106,7 +115,7 @@ def coerce(cls, pub: SamplerPubLike, shots: int | None = None) -> SamplerPub: 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=False) + return cls(circuit=circuit, parameter_values=parameter_values, shots=shots, validate=True) def validate(self): """Validate the pub.""" @@ -116,7 +125,7 @@ def validate(self): self.parameter_values.validate() if self.shots is not None: - if not isinstance(self.shots, Integral): + 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") diff --git a/test/python/primitives/containers/test_sampler_pub.py b/test/python/primitives/containers/test_sampler_pub.py new file mode 100644 index 000000000000..d26f17193636 --- /dev/null +++ b/test/python/primitives/containers/test_sampler_pub.py @@ -0,0 +1,337 @@ +# 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. + +"""Test SamplerPub class""" + +import ddt +import numpy as np + +from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.primitives.containers import SamplerPub, BindingsArray +from qiskit.test import QiskitTestCase + + +@ddt.ddt +class SamplerPubCase(QiskitTestCase): + """Test the SamplerPub class.""" + + def test_properties(self): + """Test SamplerPub properties.""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + circuit.measure_all() + parameter_values = BindingsArray(kwvals={params: np.ones((10, 2))}) + shots = 1000 + + pub = SamplerPub( + circuit=circuit, + parameter_values=parameter_values, + shots=shots, + ) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual( + pub.parameter_values, + parameter_values, + msg="incorrect value for `parameter_values` property", + ) + self.assertEqual(pub.shots, shots, msg="incorrect value for `shots` property") + + def test_invalidate_circuit(self): + """Test validation of circuit argument""" + # Invalid circuit, it is an instruction + circuit = QuantumCircuit(3).to_instruction() + with self.assertRaises(TypeError): + SamplerPub(circuit) + + @ddt.data(100.0, True, False, 100j, 1e5) + def test_invalidate_shots_type(self, shots): + """Test validation of shots argument type""" + with self.assertRaises(TypeError, msg=f"shots type {type(shots)} should raise TypeError"): + SamplerPub(QuantumCircuit(), shots=shots) + + def test_invalidate_shots_value(self): + """Test invalid shots argument value""" + with self.assertRaises(ValueError, msg=f"negative shots should raise ValueError"): + SamplerPub(QuantumCircuit(), shots=-1) + + def test_shaped_zero_parameter_values(self): + """Test Passing in a shaped array with no parameters works""" + circuit = QuantumCircuit(2) + shape = (3,) + parameter_values = BindingsArray(np.zeros((*shape, 0)), shape=shape) + pub = SamplerPub(circuit, parameter_values=parameter_values) + self.assertEqual(pub.shape, shape) + + @ddt.idata(range(5)) + def test_validate_no_parameters(self, num_params): + """Test unparameterized circuit raises for parameter values""" + circuit = QuantumCircuit(2) + parameter_values = BindingsArray(np.zeros((2, num_params)), shape=2) + if num_params == 0: + SamplerPub(circuit, parameter_values=parameter_values) + return + + with self.assertRaises(ValueError): + SamplerPub(circuit, parameter_values=parameter_values) + + @ddt.idata(range(5)) + def test_validate_num_parameters(self, num_params): + """Test unparameterized circuit raises for parameter values""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + circuit.measure_all() + parameter_values = BindingsArray(np.zeros((2, num_params)), shape=2) + if num_params == len(params): + SamplerPub(circuit, parameter_values=parameter_values) + return + + with self.assertRaises(ValueError): + SamplerPub(circuit, parameter_values=parameter_values) + + @ddt.data((), (3,), (2, 3)) + def test_shaped_zero_parameter_values(self, shape): + """Test Passing in a shaped array with no parameters works""" + circuit = QuantumCircuit(2) + parameter_values = BindingsArray(np.zeros((*shape, 0)), shape=shape) + pub = SamplerPub(circuit, parameter_values=parameter_values) + self.assertEqual(pub.shape, shape) + + def test_coerce_circuit(self): + """Test coercing an unparameterized circuit""" + circuit = QuantumCircuit(10) + circuit.measure_all() + + pub = SamplerPub.coerce(circuit) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.shots, None, msg="incorrect value for `shots` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, (), msg="incorrect shape for `parameter_values` property" + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 0, + msg="incorrect num parameters for `parameter_values` property", + ) + + def test_invalid_coerce_circuit(self): + """Test coercing parameterized circuit raises""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(10) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + circuit.measure_all() + with self.assertRaises(ValueError): + SamplerPub.coerce(circuit) + + @ddt.data(1, 10, 100, 1000) + def test_coerce_pub_with_shots(self, shots): + """Test coercing a SamplerPub""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + circuit.measure_all() + pub1 = SamplerPub( + circuit=circuit, + parameter_values=BindingsArray(kwvals={params: np.ones((10, 2))}), + shots=1000, + ) + pub2 = SamplerPub.coerce(pub1, shots=shots) + self.assertEqual(pub1, pub2) + + @ddt.data(1, 10, 100, 1000) + def test_coerce_pub_without_shots(self, shots): + """Test coercing a SamplerPub""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + circuit.measure_all() + pub1 = SamplerPub( + circuit=circuit, + parameter_values=BindingsArray(kwvals={params: np.ones((10, 2))}), + shots=None, + ) + pub2 = SamplerPub.coerce(pub1, shots=shots) + self.assertEqual(pub1.circuit, pub2.circuit, msg="incorrect value for `circuit` property") + self.assertEqual( + pub1.parameter_values, + pub2.parameter_values, + msg="incorrect value for `parameter_values` property", + ) + self.assertEqual(pub2.shots, shots, msg="incorrect value for `shots` property") + + @ddt.data(None, 1, 100) + def test_coerce_tuple_1(self, shots): + """Test coercing circuit and parameter values""" + circuit = QuantumCircuit(2) + circuit.measure_all() + pub = SamplerPub.coerce((circuit,), shots=shots) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.shots, shots, msg="incorrect value for `shots` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, (), msg="incorrect shape for `parameter_values` property" + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 0, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 1, 100) + def test_coerce_tuple_2(self, shots): + """Test coercing circuit and parameter values""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + circuit.measure_all() + parameter_values = np.zeros((4, 3, 2)) + pub = SamplerPub.coerce((circuit, parameter_values), shots=shots) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.shots, shots, msg="incorrect value for `shots` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, + (4, 3), + msg="incorrect shape for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 2, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 1, 100) + def test_coerce_tuple_2_trivial_params(self, shots): + """Test coercing circuit and parameter values""" + circuit = QuantumCircuit(2) + circuit.measure_all() + pub = SamplerPub.coerce((circuit, None), shots=shots) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.shots, shots, msg="incorrect value for `shots` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, (), msg="incorrect shape for `parameter_values` property" + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 0, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 1, 100) + def test_coerce_tuple_3(self, shots): + """Test coercing circuit and parameter values""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + circuit.measure_all() + parameter_values = np.zeros((4, 3, 2)) + pub = SamplerPub.coerce((circuit, parameter_values, 1000), shots=shots) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.shots, 1000, msg="incorrect value for `shots` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, + (4, 3), + msg="incorrect shape for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 2, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 1, 100) + def test_coerce_tuple_3_trivial_shots(self, shots): + """Test coercing circuit and parameter values""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + circuit.measure_all() + parameter_values = np.zeros((4, 3, 2)) + pub = SamplerPub.coerce((circuit, parameter_values, None), shots=shots) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.shots, shots, msg="incorrect value for `shots` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, + (4, 3), + msg="incorrect shape for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 2, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 1, 100) + def test_coerce_tuple_3_trivial_params_shots(self, shots): + """Test coercing circuit and parameter values""" + circuit = QuantumCircuit(2) + circuit.measure_all() + pub = SamplerPub.coerce((circuit, None, None), shots=shots) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.shots, shots, msg="incorrect value for `shots` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, (), msg="incorrect shape for `parameter_values` property" + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 0, + msg="incorrect num parameters for `parameter_values` property", + ) From 07f1d9102a30b609df7a5448ffb97c88f38d3bec Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 10 Jan 2024 17:29:12 -0500 Subject: [PATCH 08/11] linting --- test/python/primitives/containers/test_sampler_pub.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/python/primitives/containers/test_sampler_pub.py b/test/python/primitives/containers/test_sampler_pub.py index d26f17193636..0ebc99f1604e 100644 --- a/test/python/primitives/containers/test_sampler_pub.py +++ b/test/python/primitives/containers/test_sampler_pub.py @@ -62,17 +62,9 @@ def test_invalidate_shots_type(self, shots): def test_invalidate_shots_value(self): """Test invalid shots argument value""" - with self.assertRaises(ValueError, msg=f"negative shots should raise ValueError"): + with self.assertRaises(ValueError, msg="negative shots should raise ValueError"): SamplerPub(QuantumCircuit(), shots=-1) - def test_shaped_zero_parameter_values(self): - """Test Passing in a shaped array with no parameters works""" - circuit = QuantumCircuit(2) - shape = (3,) - parameter_values = BindingsArray(np.zeros((*shape, 0)), shape=shape) - pub = SamplerPub(circuit, parameter_values=parameter_values) - self.assertEqual(pub.shape, shape) - @ddt.idata(range(5)) def test_validate_no_parameters(self, num_params): """Test unparameterized circuit raises for parameter values""" From 9fea20205e3de1b874e64e59008ed9c5a5fee6fe Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 16 Jan 2024 14:24:35 -0500 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Ian Hincks --- qiskit/primitives/base/base_sampler.py | 15 ++++++++------- qiskit/primitives/containers/sampler_pub.py | 12 +++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 2bd34c9acfc2..2bfd37996bb1 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -18,13 +18,15 @@ :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 two values that, together, +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. @@ -56,8 +58,8 @@ # initialization of the sampler sampler = Sampler() - # run a sampler job on the Bell circuit - job = sampler.run([bell]) + # 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}")) @@ -267,9 +269,8 @@ class BaseSamplerV2: A Sampler returns samples of quantum circuit outputs. - A Sampler implementation must treat the :meth:`.run` method ``shots=None`` kwarg - as using a default ``shots`` value. The default value and methods to set it can - be determined by the Sampler implementor. + 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 @@ -280,7 +281,7 @@ def run(self, pubs: Iterable[SamplerPubLike], shots: int | None = None) -> Job: 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 + that does not specify its own shots. If ``None``, the primitive's default shots value will be used, which can vary by implementation. Returns: diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py index c60b1f7689b8..14cd6277a2c1 100644 --- a/qiskit/primitives/containers/sampler_pub.py +++ b/qiskit/primitives/containers/sampler_pub.py @@ -35,8 +35,6 @@ class SamplerPub(ShapedMixin): if ``shots=None`` the number of run shots is determined by the sampler. """ - __slots__ = ("_circuit", "_parameter_values", "_shots") - def __init__( self, circuit: QuantumCircuit, @@ -49,8 +47,9 @@ def __init__( Args: circuit: A quantum circuit. parameter_values: A bindings array. - shots: A specific number of shots to run with. - validate: If True, the input data is validated during initialization. + 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 @@ -72,7 +71,10 @@ def parameter_values(self) -> BindingsArray: @property def shots(self) -> int | None: - """An specific number of shots to run with (optional).""" + """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 From 7080cd5ab8735027b0929c2bfe45a644eb0327f6 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 16 Jan 2024 14:43:33 -0500 Subject: [PATCH 10/11] linting --- qiskit/primitives/containers/sampler_pub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/containers/sampler_pub.py b/qiskit/primitives/containers/sampler_pub.py index 14cd6277a2c1..76d5a68e6980 100644 --- a/qiskit/primitives/containers/sampler_pub.py +++ b/qiskit/primitives/containers/sampler_pub.py @@ -47,7 +47,7 @@ def __init__( Args: circuit: A quantum circuit. parameter_values: A bindings array. - shots: A specific number of shots to run with. This value takes + 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. """ @@ -72,7 +72,7 @@ def parameter_values(self) -> BindingsArray: @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 From 4e56f1e612186a414d08b91ad607db1cc0984d61 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 16 Jan 2024 16:41:09 -0500 Subject: [PATCH 11/11] Update job return type --- qiskit/primitives/base/base_sampler.py | 7 ++++++- test/python/primitives/containers/test_sampler_pub.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 2bfd37996bb1..9cf315e84fd8 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -144,9 +144,12 @@ from qiskit.providers import JobV1 as Job from qiskit.utils.deprecation import deprecate_func +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) @@ -274,7 +277,9 @@ class BaseSamplerV2: """ @abstractmethod - def run(self, pubs: Iterable[SamplerPubLike], shots: int | None = None) -> Job: + def run( + self, pubs: Iterable[SamplerPubLike], shots: int | None = None + ) -> BasePrimitiveJob[PrimitiveResult[PubResult]]: """Run and collect samples from each pub. Args: diff --git a/test/python/primitives/containers/test_sampler_pub.py b/test/python/primitives/containers/test_sampler_pub.py index 0ebc99f1604e..611c20f9c2a3 100644 --- a/test/python/primitives/containers/test_sampler_pub.py +++ b/test/python/primitives/containers/test_sampler_pub.py @@ -21,7 +21,7 @@ @ddt.ddt -class SamplerPubCase(QiskitTestCase): +class SamplerPubTestCase(QiskitTestCase): """Test the SamplerPub class.""" def test_properties(self):