diff --git a/docs/apidoc/tools.rst b/docs/apidoc/tools.rst deleted file mode 100644 index c1dd7018a6d9..000000000000 --- a/docs/apidoc/tools.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit-tools: - -.. automodule:: qiskit.tools - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/qiskit/assembler/assemble_circuits.py b/qiskit/assembler/assemble_circuits.py index 06d056a05f89..b27fe47a02e6 100644 --- a/qiskit/assembler/assemble_circuits.py +++ b/qiskit/assembler/assemble_circuits.py @@ -34,7 +34,7 @@ converters, QobjHeader, ) -from qiskit.tools.parallel import parallel_map +from qiskit.utils.parallel import parallel_map PulseLibrary = Dict[str, List[complex]] diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 4cfc06d153a4..22608394c371 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1955,7 +1955,9 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: def add_bits(self, bits: Iterable[Bit]) -> None: """Add Bits to the circuit.""" - duplicate_bits = set(self._qubit_indices).union(self._clbit_indices).intersection(bits) + duplicate_bits = { + bit for bit in bits if bit in self._qubit_indices or bit in self._clbit_indices + } if duplicate_bits: raise CircuitError(f"Attempted to add bits found already in circuit: {duplicate_bits}") diff --git a/qiskit/compiler/scheduler.py b/qiskit/compiler/scheduler.py index b714b8d46923..0a30b07a49b1 100644 --- a/qiskit/compiler/scheduler.py +++ b/qiskit/compiler/scheduler.py @@ -25,7 +25,7 @@ from qiskit.providers.backend import Backend from qiskit.scheduler import ScheduleConfig from qiskit.scheduler.schedule_circuit import schedule_circuit -from qiskit.tools.parallel import parallel_map +from qiskit.utils.parallel import parallel_map logger = logging.getLogger(__name__) diff --git a/qiskit/passmanager/passmanager.py b/qiskit/passmanager/passmanager.py index 74d5feb91088..f480ff1c3954 100644 --- a/qiskit/passmanager/passmanager.py +++ b/qiskit/passmanager/passmanager.py @@ -21,7 +21,7 @@ import dill -from qiskit.tools.parallel import parallel_map +from qiskit.utils.parallel import parallel_map from .base_tasks import Task, PassManagerIR from .exceptions import PassManagerError from .flow_controllers import FlowControllerLinear diff --git a/qiskit/tools/visualization.py b/qiskit/primitives/containers/__init__.py similarity index 62% rename from qiskit/tools/visualization.py rename to qiskit/primitives/containers/__init__.py index c3e2ff06cc33..5367d4594008 100644 --- a/qiskit/tools/visualization.py +++ b/qiskit/primitives/containers/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2019. +# (C) Copyright IBM 2023. # # 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 @@ -10,7 +10,12 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=unused-wildcard-import,missing-module-docstring,wildcard-import +""" +Data containers for primitives. +""" -# NOTE(mtreinish): Import public API here to maintain backwards compat -from qiskit.visualization import * +from .bindings_array import BindingsArray +from .data_bin import make_data_bin +from .observables_array import ObservablesArray +from .primitive_result import PrimitiveResult +from .pub_result import PubResult diff --git a/qiskit/primitives/containers/bindings_array.py b/qiskit/primitives/containers/bindings_array.py new file mode 100644 index 000000000000..14efabdc396d --- /dev/null +++ b/qiskit/primitives/containers/bindings_array.py @@ -0,0 +1,354 @@ +# 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. + +""" +Bindings array class +""" +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from itertools import chain +from typing import Union + +import numpy as np +from numpy.typing import ArrayLike + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.parameterexpression import ParameterValueType + +from .shape import ShapedMixin, ShapeInput, shape_tuple + +ParameterLike = Union[Parameter, str] + + +class BindingsArray(ShapedMixin): + r"""Stores parameter binding value sets for a :class:`qiskit.QuantumCircuit`. + + A single parameter binding set provides numeric values to bind to a circuit with free + :class:`qiskit.circuit.Parameter`\s. An instance of this class stores an array-valued + collection of such sets. The simplest example is a 0-d array consisting of a single + parameter binding set, whereas an n-d array of parameter binding sets represents an + n-d sweep over values. + + The storage format is a list of arrays, ``[vals0, vals1, ...]``, as well as a dictionary of + arrays attached to parameters, ``{params0: kwvals0, ...}``. A convention is used + where the last dimension of each array indexes (a subset of) circuit parameters. For + example, if the last dimension of ``vals1`` is 25, then it represents an array of + possible binding values for 25 distinct parameters, where its leading shape is the + array :attr:`~.shape` of its binding array. This implies a degeneracy of the storage + format: ``[vals, vals1[..., :10], vals1[..., 10:], ...]`` is exactly equivalent to + ``[vals0, vals1, ...]`` in the bindings it specifies. This allows flexibility about whether + values for different parameters are stored in one big array, or across several smaller + arrays. It also allows different parameters to use different dtypes. + + .. code-block:: python + + # 0-d array (i.e. only one binding) + BindingsArray([1, 2, 3], {"a": 4, ("b", "c"): [5, 6]}) + + # single array, last index is parameters + BindingsArray(np.empty((10, 10, 100))) + + # multiple arrays, where each last index is parameters. notice that it's smart enough to + # figure out that a missing last dimension corresponds to a single parameter. + BindingsArray( + [np.empty((10, 10, 100)), np.empty((10, 10)), np.empty((10, 10, 20), dtype=complex)], + {("c", "a"): np.empty((10, 10, 2)), "b": np.empty((10, 10))} + ) + """ + __slots__ = ("_vals", "_kwvals") + + def __init__( + self, + vals: ArrayLike | Iterable[ArrayLike] | None = None, + kwvals: Mapping[ParameterLike, Iterable[ParameterValueType]] | ArrayLike | None = None, + shape: ShapeInput | None = None, + ): + r""" + Initialize a ``BindingsArray``. It can take parameter vectors and dictionaries. + + The ``shape`` argument does not need to be provided whenever it can unambiguously + be inferred from the provided arrays. Ambiguity arises because an array provided to the + constructor might represent values for either a single parameter, with an implicit missing + last dimension of size ``1``, or for many parameters, where the size of the last dimension + is the number of parameters it is providing values to. This ambiguity can be broken in the + following common ways: + + * Only a single array is provided to ``vals``, and no arrays to ``kwvals``, in which case + it is assumed that the last dimension is over many parameters. + * Multiple arrays are given whose shapes differ only in the last dimension size. + * Some array is given in ``kwvals`` where the key contains multiple + :class:`~.Parameter`\s, whose length the last dimension of the array must therefore match. + + Args: + vals: One or more arrays, where the last index of each corresponds to + distinct parameters. If their dtypes allow it, concatenating these + arrays over the last axis is equivalent to providing them separately. + kwvals: A mapping from one or more parameters to arrays of values to bind + them to, where the last axis is over parameters. + shape: The leading shape of every array in these bindings. + + Raises: + ValueError: If all inputs are ``None``. + ValueError: If the shape cannot be automatically inferred from the arrays, or if there + is some inconsistency in the shape of the given arrays. + """ + super().__init__() + + if vals is None: + vals = [] + if kwvals is None: + kwvals = {} + + vals = [vals] if isinstance(vals, np.ndarray) else [np.array(v, copy=False) for v in vals] + # TODO str will be used for internal data (_kwvals) instead of Parameter. + # This requires https://github.com/Qiskit/qiskit/issues/7107 + kwvals = { + (p,) if isinstance(p, Parameter) else tuple(p): np.array(val, copy=False) + for p, val in kwvals.items() + } + + if shape is None: + # jump through hoops to find out user's intended shape + shape = _infer_shape(vals, kwvals) + + # shape checking, and normalization so that each last index must be over parameters + self._shape = shape_tuple(shape) + for idx, val in enumerate(vals): + vals[idx] = _standardize_shape(val, self._shape) + + self._vals: list[np.ndarray] = vals + self._kwvals = kwvals + + self.validate() + + def __getitem__(self, args) -> BindingsArray: + # because the parameters live on the last axis, we don't need to do anything special to + # accomodate them because there will always be an implicit slice(None, None, None) + # on all unspecified trailing dimensions + # separately, we choose to not disallow args which touch the last dimension, even though it + # would not be a particularly friendly way to chop parameters + vals = [val[args] for val in self._vals] + kwvals = {params: val[args] for params, val in self._kwvals.items()} + try: + shape = next(chain(vals, kwvals.values())).shape[:-1] + except StopIteration: + shape = () + return BindingsArray(vals, kwvals, shape) + + @property + def kwvals(self) -> dict[tuple[str, ...], np.ndarray]: + """The keyword values of this array.""" + return {_format_key(k): v for k, v in self._kwvals.items()} + + @property + def num_parameters(self) -> int: + """The total number of parameters.""" + return sum(val.shape[-1] for val in chain(self.vals, self._kwvals.values())) + + @property + def vals(self) -> list[np.ndarray]: + """The non-keyword values of this array.""" + return self._vals + + def bind(self, circuit: QuantumCircuit, loc: tuple[int, ...]) -> QuantumCircuit: + """Return a new circuit bound to the values at the provided index. + + Args: + circuit: The circuit to bind. + loc: A tuple of indices, on for each dimension of this array. + + Returns: + The bound circuit. + + Raises: + ValueError: If the index doesn't have the right number of values. + """ + if len(loc) != self.ndim: + raise ValueError(f"Expected {loc} to index all dimensions of {self.shape}") + + flat_vals = (val for vals in self.vals for val in vals[loc]) + + if not self._kwvals: + # special case to avoid constructing a dictionary input + return circuit.assign_parameters(list(flat_vals)) + + parameters = dict(zip(circuit.parameters, flat_vals)) + parameters.update( + (param, val) + for params, vals in self._kwvals.items() + for param, val in zip(params, vals[loc]) + ) + return circuit.assign_parameters(parameters) + + def bind_all(self, circuit: QuantumCircuit) -> np.ndarray: + """Return an object array of bound circuits with the same shape. + + Args: + circuit: The circuit to bind. + + Returns: + An object array of the same shape containing all bound circuits. + """ + arr = np.empty(self.shape, dtype=object) + for idx in np.ndindex(self.shape): + arr[idx] = self.bind(circuit, idx) + return arr + + def ravel(self) -> BindingsArray: + """Return a new :class:`~BindingsArray` with one dimension. + + The returned bindings array has a :attr:`shape` given by ``(size, )``, where the size is the + :attr:`~size` of this bindings array. + + Returns: + A new bindings array. + """ + return self.reshape(self.size) + + def reshape(self, shape: int | Iterable[int]) -> BindingsArray: + """Return a new :class:`~BindingsArray` with a different shape. + + This results in a new view of the same arrays. + + Args: + shape: The shape of the returned bindings array. + + Returns: + A new bindings array. + + Raises: + ValueError: If the provided shape has a different product than the current size. + """ + shape = (shape, -1) if isinstance(shape, int) else (*shape, -1) + if np.prod(shape[:-1]).astype(int) != self.size: + raise ValueError("Reshaping cannot change the total number of elements.") + vals = [val.reshape(shape) for val in self._vals] + kwvals = {params: val.reshape(shape) for params, val in self._kwvals.items()} + return BindingsArray(vals, kwvals, shape[:-1]) + + @classmethod + def coerce(cls, bindings_array: BindingsArrayLike) -> BindingsArray: + """Coerce an input that is :class:`~BindingsArrayLike` into a new :class:`~BindingsArray`. + + Args: + bindings_array: An object to be bindings array. + + Returns: + A new bindings array. + """ + if isinstance(bindings_array, Sequence): + bindings_array = np.array(bindings_array) + if bindings_array is None: + bindings_array = cls() + elif isinstance(bindings_array, np.ndarray): + bindings_array = cls(bindings_array) + elif isinstance(bindings_array, Mapping): + bindings_array = cls(kwvals=bindings_array) + else: + raise TypeError(f"Unsupported type {type(bindings_array)} is given.") + return bindings_array + + def validate(self): + """Validate the consistency in bindings_array.""" + for parameters, val in self._kwvals.items(): + val = self._kwvals[parameters] = _standardize_shape(val, self._shape) + if len(parameters) != val.shape[-1]: + raise ValueError( + f"Length of {parameters} inconsistent with last dimension of {val}" + ) + + +def _standardize_shape(val: np.ndarray, shape: tuple[int, ...]) -> np.ndarray: + """Return ``val`` or ``val[..., None]``. + + Args: + val: The array whose shape to standardize. + shape: The shape to standardize to. + + Returns: + An array with one more dimension than ``len(shape)``, and whose leading dimensions match + ``shape``. + + Raises: + ValueError: If the leading shape of ``val`` does not match the ``shape``. + """ + if val.shape == shape: + val = val[..., None] + elif val.ndim - 1 != len(shape) or val.shape[:-1] != shape: + raise ValueError(f"Array with shape {val.shape} inconsistent with {shape}") + return val + + +def _infer_shape( + vals: list[np.ndarray], kwvals: dict[tuple[Parameter, ...], np.ndarray] +) -> tuple[int, ...]: + """Return a shape tuple that consistently defines the leading dimensions of all arrays. + + Args: + vals: A list of arrays. + kwvals: A mapping from tuples to arrays, where the length of each tuple should match the + last dimension of the corresponding array. + + Returns: + A shape tuple that matches the leading dimension of every array. + + Raises: + ValueError: If this cannot be done unambiguously. + """ + only_possible_shapes = None + + def examine_array(*possible_shapes): + nonlocal only_possible_shapes + if only_possible_shapes is None: + only_possible_shapes = set(possible_shapes) + else: + only_possible_shapes.intersection_update(possible_shapes) + + for parameters, val in kwvals.items(): + if len(parameters) > 1: + # here, the last dimension _has_ to be over parameters + examine_array(val.shape[:-1]) + elif val.shape == () or val.shape == (1,) or val.shape[-1] != 1: + # here, if the last dimension is not 1 or shape is () or (1,) then the shape is the shape + examine_array(val.shape) + else: + # here, the last dimension could be over parameters or not + examine_array(val.shape, val.shape[:-1]) + + if len(vals) == 1 and len(kwvals) == 0: + examine_array(vals[0].shape[:-1]) + elif len(vals) == 0 and len(kwvals) == 0: + examine_array(()) + else: + for val in vals: + # here, the last dimension could be over parameters or not + examine_array(val.shape, val.shape[:-1]) + + if len(only_possible_shapes) == 1: + return next(iter(only_possible_shapes)) + elif len(only_possible_shapes) == 0: + raise ValueError("Could not find any consistent shape.") + raise ValueError("Could not unambiguously determine the intended shape; specify shape manually") + + +def _format_key(key: tuple[Parameter | str, ...]): + return tuple(k.name if isinstance(k, Parameter) else k for k in key) + + +BindingsArrayLike = Union[ + BindingsArray, + ArrayLike, + "Mapping[Parameter, ArrayLike]", + "Sequence[ArrayLike]", + None, +] diff --git a/qiskit/primitives/containers/data_bin.py b/qiskit/primitives/containers/data_bin.py new file mode 100644 index 000000000000..b4e479266e72 --- /dev/null +++ b/qiskit/primitives/containers/data_bin.py @@ -0,0 +1,89 @@ +# 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. + +""" +Dataclass tools for data namespaces (bins) +""" +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import make_dataclass + + +class DataBinMeta(type): + """Metaclass for :class:`DataBin` that adds the shape to the type name. + + This is so that the class has a custom repr with DataBin<*shape> notation. + """ + + def __repr__(cls): + name = cls.__name__ + if cls._SHAPE is None: + return name + shape = ",".join(map(str, cls._SHAPE)) + return f"{name}<{shape}>" + + +class DataBin(metaclass=DataBinMeta): + """Base class for data bin containers. + + Subclasses are typically made via :class:`~make_data_bin`, which is a specialization of + :class:`make_dataclass`. + """ + + _RESTRICTED_NAMES = ("_RESTRICTED_NAMES", "_SHAPE", "_FIELDS", "_FIELD_TYPES") + _SHAPE: tuple[int, ...] | None = None + _FIELDS: tuple[str, ...] = () + """The fields allowed in this data bin.""" + _FIELD_TYPES: tuple[type, ...] = () + """The types of each field.""" + + def __repr__(self): + vals = (f"{name}={getattr(self, name)}" for name in self._FIELDS if hasattr(self, name)) + return f"{type(self)}({', '.join(vals)})" + + +def make_data_bin( + fields: Iterable[tuple[str, type]], shape: tuple[int, ...] | None = None +) -> DataBinMeta: + """Return a new subclass of :class:`~DataBin` with the provided fields and shape. + + .. code-block:: python + + my_bin = make_data_bin([("alpha", np.NDArray[np.float])], shape=(20, 30)) + + # behaves like a dataclass + my_bin(alpha=np.empty((20, 30))) + + Args: + fields: Tuples ``(name, type)`` specifying the attributes of the returned class. + shape: The intended shape of every attribute of this class. + + Returns: + A new class. + """ + field_names, field_types = zip(*fields) + for name in field_names: + if name in DataBin._RESTRICTED_NAMES: + raise ValueError(f"'{name}' is a restricted name for a DataBin.") + cls = make_dataclass( + "DataBin", + dict(zip(field_names, field_types)), + bases=(DataBin,), + frozen=True, + unsafe_hash=True, + repr=False, + ) + cls._SHAPE = shape + cls._FIELDS = field_names + cls._FIELD_TYPES = field_types + return cls diff --git a/qiskit/primitives/containers/object_array.py b/qiskit/primitives/containers/object_array.py new file mode 100644 index 000000000000..f93515daf898 --- /dev/null +++ b/qiskit/primitives/containers/object_array.py @@ -0,0 +1,94 @@ +# 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. + +""" +Object ND-array initialization function. +""" +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np +from numpy.typing import ArrayLike + + +def object_array( + arr: ArrayLike, + order: str | None = None, + copy: bool = True, + list_types: Sequence[type] | None = (), +) -> np.ndarray: + """Convert an array-like of objects into an object array. + + .. note:: + + If the objects in the array like input define ``__array__`` methods + this avoids calling them and will instead set the returned array values + to the Python objects themselves. + + Args: + arr: An array-like input. + order: Optional, the order of the returned array (C, F, A, K). If None + the default NumPy ordering of C is used. + copy: If True make a copy of the input if it is already an array. + list_types: Optional, a sequence of types to treat as lists of array + element objects when inferring the array shape from the input. + + Returns: + A NumPy ND-array with ``dtype=object``. + + Raises: + ValueError: If the input cannot be coerced into an object array. + """ + if isinstance(arr, np.ndarray): + if arr.dtype != object or order is not None or copy is True: + arr = arr.astype(object, order=order, copy=copy) + return arr + + shape = _infer_shape(arr, list_types=tuple(list_types)) + obj_arr = np.empty(shape, dtype=object, order=order) + if not shape: + # We call fill here instead of [()] to avoid invoking the + # objects `__array__` method if it has one (eg for Pauli's). + obj_arr.fill(arr) + else: + # For other arrays we need to do some tricks to avoid invoking the + # objects __array__ method by flattening the input and initializing + # using `np.fromiter` which does not invoke `__array__` for object + # dtypes. + def _flatten(nested, k): + if k == 1: + return nested + else: + return [item for sublist in nested for item in _flatten(sublist, k - 1)] + + flattened = _flatten(arr, len(shape)) + if len(flattened) != obj_arr.size: + raise ValueError( + "Input object size does not match the inferred array shape." + " This most likely occurs when the input is a ragged array." + ) + obj_arr.flat = np.fromiter(flattened, dtype=object, count=len(flattened)) + + return obj_arr + + +def _infer_shape(obj: ArrayLike, list_types: tuple[type, ...] = ()) -> tuple[int, ...]: + """Infer the shape of an array-like object without casting""" + if isinstance(obj, np.ndarray): + return obj.shape + if not isinstance(obj, (list, *list_types)): + return () + size = len(obj) + if size == 0: + return (size,) + return (size, *_infer_shape(obj[0], list_types=list_types)) diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py new file mode 100644 index 000000000000..0d33111d5957 --- /dev/null +++ b/qiskit/primitives/containers/observables_array.py @@ -0,0 +1,265 @@ +# 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. + + +""" +ND-Array container class for Estimator observables. +""" +from __future__ import annotations + +import re +from collections import defaultdict +from collections.abc import Mapping as MappingType +from functools import lru_cache +from typing import Iterable, Mapping, Union, overload + +import numpy as np +from numpy.typing import ArrayLike + +from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp + +from .object_array import object_array +from .shape import ShapedMixin + +BasisObservable = Mapping[str, complex] +"""Representation type of a single observable.""" + +BasisObservableLike = Union[ + str, + Pauli, + SparsePauliOp, + Mapping[Union[str, Pauli], complex], + Iterable[Union[str, Pauli, SparsePauliOp]], +] +"""Types that can be natively used to construct a :const:`BasisObservable`.""" + + +class ObservablesArray(ShapedMixin): + """An ND-array of :const:`.BasisObservable` for an :class:`.Estimator` primitive.""" + + __slots__ = ("_array", "_shape") + ALLOWED_BASIS: str = "IXYZ01+-lr" + """The allowed characters in :const:`BasisObservable` strings.""" + + def __init__( + self, + observables: BasisObservableLike | ArrayLike, + copy: bool = True, + validate: bool = True, + ): + """Initialize an observables array. + + Args: + observables: An array-like of basis observable compatible objects. + copy: Specify the ``copy`` kwarg of the :func:`.object_array` function + when initializing observables. + validate: If True, convert :const:`.BasisObservableLike` input objects + to :const:`.BasisObservable` objects and validate. If False the + input should already be an array-like of valid + :const:`.BasisObservble` objects. + + Raises: + ValueError: If ``validate=True`` and the input observables is not valid. + """ + super().__init__() + if isinstance(observables, ObservablesArray): + observables = observables._array + self._array = object_array(observables, copy=copy, list_types=(PauliList,)) + self._shape = self._array.shape + if validate: + num_qubits = None + for ndi, obs in np.ndenumerate(self._array): + basis_obs = self.format_observable(obs) + basis_num_qubits = len(next(iter(basis_obs))) + if num_qubits is None: + num_qubits = basis_num_qubits + elif basis_num_qubits != num_qubits: + raise ValueError( + "The number of qubits must be the same for all observables in the " + "observables array." + ) + self._array[ndi] = basis_obs + + def __repr__(self): + prefix = f"{type(self).__name__}(" + suffix = f", shape={self.shape})" + array = np.array2string(self._array, prefix=prefix, suffix=suffix, threshold=50) + return prefix + array + suffix + + def tolist(self) -> list: + """Convert to a nested list""" + return self._array.tolist() + + def __array__(self, dtype=None): + """Convert to an Numpy.ndarray""" + if dtype is None or dtype == object: + return self._array + raise ValueError("Type must be 'None' or 'object'") + + @overload + def __getitem__(self, args: int | tuple[int, ...]) -> BasisObservable: + ... + + @overload + def __getitem__(self, args: slice) -> ObservablesArray: + ... + + def __getitem__(self, args): + item = self._array[args] + if not isinstance(item, np.ndarray): + return item + return ObservablesArray(item, copy=False, validate=False) + + def reshape(self, shape: int | Iterable[int]) -> ObservablesArray: + """Return a new array with a different shape. + + This results in a new view of the same arrays. + + Args: + shape: The shape of the returned array. + + Returns: + A new array. + """ + return ObservablesArray(self._array.reshape(shape), copy=False, validate=False) + + def ravel(self) -> ObservablesArray: + """Return a new array with one dimension. + + The returned array has a :attr:`shape` given by ``(size, )``, where + the size is the :attr:`~size` of this array. + + Returns: + A new flattened array. + """ + return self.reshape(self.size) + + @classmethod + def format_observable(cls, observable: BasisObservableLike) -> BasisObservable: + """Format an observable-like object into a :const:`BasisObservable`. + + Args: + observable: The observable-like to format. + + Returns: + The given observable as a :const:`~BasisObservable`. + + Raises: + TypeError: If the input cannot be formatted because its type is not valid. + ValueError: If the input observable is invalid. + """ + + # Pauli-type conversions + if isinstance(observable, SparsePauliOp): + # Call simplify to combine duplicate keys before converting to a mapping + return cls.format_observable(dict(observable.simplify(atol=0).to_list())) + + if isinstance(observable, Pauli): + label, phase = observable[:].to_label(), observable.phase + return {label: 1} if phase == 0 else {label: (-1j) ** phase} + + # String conversion + if isinstance(observable, str): + cls._validate_basis(observable) + return {observable: 1} + + # Mapping conversion (with possible Pauli keys) + if isinstance(observable, MappingType): + num_qubits = len(next(iter(observable))) + unique = defaultdict(complex) + for basis, coeff in observable.items(): + if isinstance(basis, Pauli): + basis, phase = basis[:].to_label(), basis.phase + if phase != 0: + coeff = coeff * (-1j) ** phase + # Validate basis + cls._validate_basis(basis) + if len(basis) != num_qubits: + raise ValueError( + "Number of qubits must be the same for all observable basis elements." + ) + unique[basis] += coeff + return dict(unique) + + raise TypeError(f"Invalid observable type: {type(observable)}") + + @classmethod + def coerce(cls, observables: ObservablesArrayLike) -> ObservablesArray: + """Coerce ObservablesArrayLike into ObservableArray. + + Args: + observables: an object to be observables array. + + Returns: + A coerced observables array. + """ + if isinstance(observables, ObservablesArray): + return observables + if isinstance(observables, (str, SparsePauliOp, Pauli, Mapping)): + observables = [observables] + return cls(observables) + + def validate(self): + """Validate the consistency in observables array.""" + num_qubits = None + for obs in self._array: + basis_num_qubits = len(next(iter(obs))) + if num_qubits is None: + num_qubits = basis_num_qubits + elif basis_num_qubits != num_qubits: + raise ValueError( + "The number of qubits must be the same for all observables in the " + "observables array." + ) + + @classmethod + def _validate_basis(cls, basis: str) -> None: + """Validate a basis string. + + Args: + basis: a basis string to validate. + + Raises: + ValueError: If basis string contains invalid characters + """ + # NOTE: the allowed basis characters can be overridden by modifying the class + # attribute ALLOWED_BASIS + allowed_pattern = _regex_match(cls.ALLOWED_BASIS) + if not allowed_pattern.match(basis): + invalid_pattern = _regex_invalid(cls.ALLOWED_BASIS) + invalid_chars = list(set(invalid_pattern.findall(basis))) + raise ValueError( + f"Observable basis string '{basis}' contains invalid characters {invalid_chars}," + f" allowed characters are {list(cls.ALLOWED_BASIS)}.", + ) + + +ObservablesArrayLike = Union[ObservablesArray, ArrayLike, BasisObservableLike] +"""Types that can be natively converted to an ObservablesArray""" + + +class PauliArray(ObservablesArray): + """An ND-array of Pauli-basis observables for an :class:`.Estimator` primitive.""" + + ALLOWED_BASIS = "IXYZ" + + +@lru_cache(1) +def _regex_match(allowed_chars: str) -> re.Pattern: + """Return pattern for matching if a string contains only the allowed characters.""" + return re.compile(f"^[{re.escape(allowed_chars)}]*$") + + +@lru_cache(1) +def _regex_invalid(allowed_chars: str) -> re.Pattern: + """Return pattern for selecting invalid strings""" + return re.compile(f"[^{re.escape(allowed_chars)}]") diff --git a/qiskit/primitives/containers/primitive_result.py b/qiskit/primitives/containers/primitive_result.py new file mode 100644 index 000000000000..b383f4e119e1 --- /dev/null +++ b/qiskit/primitives/containers/primitive_result.py @@ -0,0 +1,52 @@ +# 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. + +"""PrimitiveResult""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any, Generic, TypeVar + +from .pub_result import PubResult + +T = TypeVar("T", bound=PubResult) + + +class PrimitiveResult(Generic[T]): + """A container for multiple pub results and global metadata.""" + + def __init__(self, pub_results: Iterable[T], metadata: dict[str, Any] | None = None): + """ + Args: + pub_results: Pub results. + metadata: Any metadata that doesn't make sense to put inside of pub results. + """ + self._pub_results = list(pub_results) + self._metadata = metadata or {} + + @property + def metadata(self) -> dict[str, Any]: + """The metadata of this primitive result.""" + return self._metadata + + def __getitem__(self, index) -> T: + return self._pub_results[index] + + def __len__(self) -> int: + return len(self._pub_results) + + def __repr__(self) -> str: + return f"PrimitiveResult({self._pub_results}, metadata={self.metadata})" + + def __iter__(self) -> Iterable[T]: + return iter(self._pub_results) diff --git a/qiskit/primitives/containers/pub_result.py b/qiskit/primitives/containers/pub_result.py new file mode 100644 index 000000000000..c7ac2b6160e7 --- /dev/null +++ b/qiskit/primitives/containers/pub_result.py @@ -0,0 +1,45 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. + +""" +Base Pub class +""" + +from __future__ import annotations + +from .data_bin import DataBin + + +class PubResult: + """Result of Primitive Unified Bloc.""" + + __slots__ = ("_data", "_metadata") + + def __init__(self, data: DataBin, metadata: dict | None = None): + """Initialize a pub result. + + Args: + data: result data bin. + metadata: metadata dictionary. + """ + self._data = data + self._metadata = metadata or {} + + @property + def data(self) -> DataBin: + """Result data for the pub.""" + return self._data + + @property + def metadata(self) -> dict: + """Metadata for the pub.""" + return self._metadata diff --git a/qiskit/primitives/containers/shape.py b/qiskit/primitives/containers/shape.py new file mode 100644 index 000000000000..952916cd67dc --- /dev/null +++ b/qiskit/primitives/containers/shape.py @@ -0,0 +1,129 @@ +# 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. + +""" +Array shape related classes and functions +""" +from __future__ import annotations + +from collections.abc import Iterable +from typing import Protocol, Union, runtime_checkable + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +ShapeInput = Union[int, "Iterable[ShapeInput]"] +"""An input that is coercible into a shape tuple.""" + + +@runtime_checkable +class Shaped(Protocol): + """Protocol that defines what it means to be a shaped object. + + Note that static type checkers will classify ``numpy.ndarray`` as being :class:`Shaped`. + Moreover, since this protocol is runtime-checkable, we will even have + ``isinstance(, Shaped) == True``. + """ + + @property + def shape(self) -> tuple[int, ...]: + """The array shape of this object.""" + raise NotImplementedError("A `Shaped` protocol must implement the `shape` property") + + @property + def ndim(self) -> int: + """The number of array dimensions of this object.""" + raise NotImplementedError("A `Shaped` protocol must implement the `ndim` property") + + @property + def size(self) -> int: + """The total dimension of this object, i.e. the product of the entries of :attr:`~shape`.""" + raise NotImplementedError("A `Shaped` protocol must implement the `size` property") + + +class ShapedMixin(Shaped): + """Mixin class to create :class:`~Shaped` types by only providing :attr:`_shape` attribute.""" + + _shape: tuple[int, ...] + + def __repr__(self): + return f"{type(self).__name__}(<{self.shape}>)" + + @property + def shape(self): + return self._shape + + @property + def ndim(self): + return len(self._shape) + + @property + def size(self): + return int(np.prod(self._shape, dtype=int)) + + +def array_coerce(arr: ArrayLike | Shaped) -> NDArray | Shaped: + """Coerce the input into an object with a shape attribute. + + Copies are avoided. + + Args: + arr: The object to coerce. + + Returns: + Something that is :class:`~Shaped`, and always ``numpy.ndarray`` if the input is not + already :class:`~Shaped`. + """ + if isinstance(arr, Shaped): + return arr + return np.array(arr, copy=False) + + +def _flatten_to_ints(arg: ShapeInput) -> Iterable[int]: + """ + Yield one integer at a time. + + Args: + arg: Integers or iterables of integers, possibly nested, to be yielded. + + Yields: + The provided integers in depth-first recursive order. + + Raises: + ValueError: If an input is not an iterable or an integer. + """ + for item in arg: + try: + if isinstance(item, Iterable): + yield from _flatten_to_ints(item) + elif int(item) == item: + yield int(item) + else: + raise ValueError(f"Expected {item} to be iterable or an integer.") + except (TypeError, RecursionError) as ex: + raise ValueError(f"Expected {item} to be iterable or an integer.") from ex + + +def shape_tuple(*shapes: ShapeInput) -> tuple[int, ...]: + """ + Flatten the input into a single tuple of integers, preserving order. + + Args: + shapes: Integers or iterables of integers, possibly nested. + + Returns: + A tuple of integers. + + Raises: + ValueError: If some member of ``shapes`` is not an integer or iterable. + """ + return tuple(_flatten_to_ints(shapes)) diff --git a/qiskit/providers/fake_provider/__init__.py b/qiskit/providers/fake_provider/__init__.py index 943869114f85..938a4273e218 100644 --- a/qiskit/providers/fake_provider/__init__.py +++ b/qiskit/providers/fake_provider/__init__.py @@ -37,7 +37,7 @@ from qiskit import QuantumCircuit from qiskit.providers.fake_provider import FakeManilaV2 from qiskit import transpile - from qiskit.tools.visualization import plot_histogram + from qiskit.visualization import plot_histogram # Get a fake backend from the fake provider diff --git a/qiskit/tools/events/__init__.py b/qiskit/tools/events/__init__.py deleted file mode 100644 index d15a537bb942..000000000000 --- a/qiskit/tools/events/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. - -""" -=================================== -Events (:mod:`qiskit.tools.events`) -=================================== - -A helper component for publishing and subscribing to events. - -.. currentmodule:: qiskit.tools.events - -.. autoclass:: TextProgressBar -""" - -from .progressbar import TextProgressBar diff --git a/qiskit/tools/events/progressbar.py b/qiskit/tools/events/progressbar.py deleted file mode 100644 index 459662e87a9b..000000000000 --- a/qiskit/tools/events/progressbar.py +++ /dev/null @@ -1,195 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. - -# This file is part of QuTiP: Quantum Toolbox in Python. -# -# Copyright (c) 2011 and later, Paul D. Nation and Robert J. Johansson. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# 3. Neither the name of the QuTiP: Quantum Toolbox in Python nor the names -# of its contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -############################################################################### - -"""Progress bars module""" - -import time -import datetime -import sys -from qiskit.tools.events.pubsub import Subscriber - - -class BaseProgressBar(Subscriber): - """An abstract progress bar with some shared functionality.""" - - def __init__(self): - super().__init__() - self.type = "progressbar" - self.touched = False - self.iter = None - self.t_start = None - self.t_done = None - - def start(self, iterations): - """Start the progress bar. - - Parameters: - iterations (int): Number of iterations. - """ - self.touched = True - self.iter = int(iterations) - self.t_start = time.time() - - def update(self, n): - """Update status of progress bar.""" - pass - - def time_elapsed(self): - """Return the time elapsed since start. - - Returns: - elapsed_time: Time since progress bar started. - """ - return "%6.2fs" % (time.time() - self.t_start) - - def time_remaining_est(self, completed_iter): - """Estimate the remaining time left. - - Parameters: - completed_iter (int): Number of iterations completed. - - Returns: - est_time: Estimated time remaining. - """ - if completed_iter: - t_r_est = (time.time() - self.t_start) / completed_iter * (self.iter - completed_iter) - else: - t_r_est = 0 - date_time = datetime.datetime(1, 1, 1) + datetime.timedelta(seconds=t_r_est) - time_string = "%02d:%02d:%02d:%02d" % ( - date_time.day - 1, - date_time.hour, - date_time.minute, - date_time.second, - ) - - return time_string - - def finished(self): - """Run when progress bar has completed.""" - pass - - -class TextProgressBar(BaseProgressBar): - """ - A simple text-based progress bar. - - output_handler : the handler the progress bar should be written to, default - is sys.stdout, another option is sys.stderr - - Examples: - - The progress bar can be used to track the progress of a `parallel_map`. - - .. code-block:: python - - import numpy as np - import qiskit.tools.jupyter - from qiskit.tools.parallel import parallel_map - from qiskit.tools.events import TextProgressBar - - TextProgressBar() - %qiskit_progress_bar -t text - parallel_map(np.sin, np.linspace(0,10,100)); - - And it can also be used individually. - - .. code-block:: python - - from qiskit.tools.events import TextProgressBar - - iterations = 100 - t = TextProgressBar() - t.start(iterations=iterations) - for i in range(iterations): - # step i of heavy calculation ... - t.update(i + 1) # update progress bar - - """ - - def __init__(self, output_handler=None): - super().__init__() - self._init_subscriber() - - self.output_handler = output_handler if output_handler else sys.stdout - - def _init_subscriber(self): - def _initialize_progress_bar(num_tasks): - self.start(num_tasks) - - self.subscribe("terra.parallel.start", _initialize_progress_bar) - - def _update_progress_bar(progress): - self.update(progress) - - self.subscribe("terra.parallel.done", _update_progress_bar) - - def _finish_progress_bar(): - self.unsubscribe("terra.parallel.start", _initialize_progress_bar) - self.unsubscribe("terra.parallel.done", _update_progress_bar) - self.unsubscribe("terra.parallel.finish", _finish_progress_bar) - self.finished() - - self.subscribe("terra.parallel.finish", _finish_progress_bar) - - def start(self, iterations): - self.touched = True - self.iter = int(iterations) - self.t_start = time.time() - pbar = "-" * 50 - self.output_handler.write("\r|{}| {}{}{} [{}]".format(pbar, 0, "/", self.iter, "")) - - def update(self, n): - # Don't update if we are not initialized or - # the update iteration number is greater than the total iterations set on start. - if not self.touched or n > self.iter: - return - filled_length = int(round(50 * n / self.iter)) - pbar = "█" * filled_length + "-" * (50 - filled_length) - time_left = self.time_remaining_est(n) - self.output_handler.write("\r|{}| {}{}{} [{}]".format(pbar, n, "/", self.iter, time_left)) - if n == self.iter: - self.output_handler.write("\n") - self.output_handler.flush() diff --git a/qiskit/tools/events/pubsub.py b/qiskit/tools/events/pubsub.py deleted file mode 100644 index 0b2a6d664679..000000000000 --- a/qiskit/tools/events/pubsub.py +++ /dev/null @@ -1,158 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. - -""" -Message broker for the Publisher / Subscriber mechanism -""" - -from qiskit.exceptions import QiskitError - - -class _Broker: - """The event/message broker. It's a singleton. - - In order to keep consistency across all the components, it would be great to - have a specific format for new events, documenting their usage. - It's the responsibility of the component emitting an event to document it's usage in - the component docstring. - - Event format: - "terra..." - - Examples: - "terra.transpiler.compile.start" - "terra.job.status.changed" - "terra.backend.run.start" - "terra.job.result.received" - """ - - _instance = None - _subscribers = {} - - def __new__(cls): - if _Broker._instance is None: - _Broker._instance = object.__new__(cls) - return _Broker._instance - - class _Subscription: - def __init__(self, event, callback): - self.event = event - self.callback = callback - - def __eq__(self, other): - """Overrides the default implementation""" - if isinstance(other, self.__class__): - return self.event == other.event and id(self.callback) == id( - other.callback - ) # Allow 1:N subscribers - return False - - def subscribe(self, event, callback): - """Subscribes to an event, so when it's emitted all the callbacks subscribed, - will be executed. We are not allowing double registration. - - Args: - event (string): The event to subscribed in the form of: - "terra..." - callback (callable): The callback that will be executed when an event is - emitted. - """ - if not callable(callback): - raise QiskitError("Callback is not a callable!") - - if event not in self._subscribers: - self._subscribers[event] = [] - - new_subscription = self._Subscription(event, callback) - if new_subscription in self._subscribers[event]: - # We are not allowing double subscription - return False - - self._subscribers[event].append(new_subscription) - return True - - def dispatch(self, event, *args, **kwargs): - """Emits an event if there are any subscribers. - - Args: - event (String): The event to be emitted - args: Arguments linked with the event - kwargs: Named arguments linked with the event - """ - # No event, no subscribers. - if event not in self._subscribers: - return - - for subscriber in self._subscribers[event]: - subscriber.callback(*args, **kwargs) - - def unsubscribe(self, event, callback): - """Unsubscribe the specific callback to the event. - - Args - event (String): The event to unsubscribe - callback (callable): The callback that won't be executed anymore - - Returns - True: if we have successfully unsubscribed to the event - False: if there's no callback previously registered - """ - - try: - self._subscribers[event].remove(self._Subscription(event, callback)) - except KeyError: - return False - - return True - - def clear(self): - """Unsubscribe everything, leaving the Broker without subscribers/events.""" - self._subscribers.clear() - - -class Publisher: - """Represents a Publisher, every component (class) can become a Publisher and - send events by inheriting this class. Functions can call this class like: - Publisher().publish("event", args, ... ) - """ - - def __init__(self): - self._broker = _Broker() - - def publish(self, event, *args, **kwargs): - """Triggers an event, and associates some data to it, so if there are any - subscribers, their callback will be called synchronously.""" - return self._broker.dispatch(event, *args, **kwargs) - - -class Subscriber: - """Represents a Subscriber, every component (class) can become a Subscriber and - subscribe to events, that will call callback functions when they are emitted. - """ - - def __init__(self): - self._broker = _Broker() - - def subscribe(self, event, callback): - """Subscribes to an event, associating a callback function to that event, so - when the event occurs, the callback will be called. - This is a blocking call, so try to keep callbacks as lightweight as possible.""" - return self._broker.subscribe(event, callback) - - def unsubscribe(self, event, callback): - """Unsubscribe a pair event-callback, so the callback will not be called anymore - when the event occurs.""" - return self._broker.unsubscribe(event, callback) - - def clear(self): - """Unsubscribe everything""" - self._broker.clear() diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index ca71ebee2777..2859fb6075c8 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -43,7 +43,7 @@ from qiskit.transpiler.passes.routing.sabre_swap import _build_sabre_dag, _apply_sabre_result from qiskit.transpiler.target import Target from qiskit.transpiler.coupling import CouplingMap -from qiskit.tools.parallel import CPU_COUNT +from qiskit.utils.parallel import CPU_COUNT logger = logging.getLogger(__name__) diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 250b310d3a80..e8d8a437ed63 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -29,7 +29,7 @@ from qiskit.transpiler.target import Target from qiskit.transpiler.passes.layout import disjoint_utils from qiskit.dagcircuit import DAGCircuit -from qiskit.tools.parallel import CPU_COUNT +from qiskit.utils.parallel import CPU_COUNT from qiskit._accelerate.sabre_swap import ( build_swap_map, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 9219854a16a3..3e29e4dc4126 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -18,7 +18,6 @@ from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit.utils.deprecation import deprecate_func from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passes import Error @@ -593,28 +592,6 @@ def _require_alignment(property_set): return scheduling -@deprecate_func( - additional_msg="Instead, use :func:`~qiskit.transpiler.preset_passmanagers.common.get_vf2_limits`.", - since="0.25.0", - package_name="qiskit-terra", -) -def get_vf2_call_limit( - optimization_level: int, - layout_method: Optional[str] = None, - initial_layout: Optional[Layout] = None, -) -> Optional[int]: - """Get the vf2 call limit for vf2 based layout passes.""" - vf2_call_limit = None - if layout_method is None and initial_layout is None: - if optimization_level == 1: - vf2_call_limit = int(5e4) # Set call limit to ~100ms with rustworkx 0.10.2 - elif optimization_level == 2: - vf2_call_limit = int(5e6) # Set call limit to ~10 sec with rustworkx 0.10.2 - elif optimization_level == 3: - vf2_call_limit = int(3e7) # Set call limit to ~60 sec with rustworkx 0.10.2 - return vf2_call_limit - - VF2Limits = collections.namedtuple("VF2Limits", ("call_limit", "max_trials")) diff --git a/qiskit/utils/__init__.py b/qiskit/utils/__init__.py index a9b73b85f95d..d1cecf05d6b0 100644 --- a/qiskit/utils/__init__.py +++ b/qiskit/utils/__init__.py @@ -30,6 +30,13 @@ .. autofunction:: wrap_method +Parallel Routines +----------------- +A helper function for calling a custom function with python +:class:`~concurrent.futures.ProcessPoolExecutor`. Tasks can be executed in parallel using this function. + +.. autofunction:: parallel_map + Optional Dependency Checkers (:mod:`qiskit.utils.optionals`) ============================================================ @@ -51,6 +58,8 @@ from . import optionals +from .parallel import parallel_map + __all__ = [ "LazyDependencyManager", "LazyImportTester", @@ -63,4 +72,5 @@ "local_hardware_info", "is_main_process", "apply_prefix", + "parallel_map", ] diff --git a/qiskit/tools/parallel.py b/qiskit/utils/parallel.py similarity index 93% rename from qiskit/tools/parallel.py rename to qiskit/utils/parallel.py index 1b48791e3477..b46c3bc7c9b8 100644 --- a/qiskit/tools/parallel.py +++ b/qiskit/utils/parallel.py @@ -54,7 +54,6 @@ from qiskit.exceptions import QiskitError from qiskit.utils.multiprocessing import local_hardware_info -from qiskit.tools.events.pubsub import Publisher from qiskit import user_config @@ -151,13 +150,6 @@ def func(_): if len(values) == 1: return [task(values[0], *task_args, **task_kwargs)] - Publisher().publish("terra.parallel.start", len(values)) - nfinished = [0] - - def _callback(_): - nfinished[0] += 1 - Publisher().publish("terra.parallel.done", nfinished[0]) - # Run in parallel if not Win and not in parallel already if ( num_processes > 1 @@ -172,18 +164,15 @@ def _callback(_): future = executor.map(_task_wrapper, param) results = list(future) - Publisher().publish("terra.parallel.done", len(results)) except (KeyboardInterrupt, Exception) as error: if isinstance(error, KeyboardInterrupt): - Publisher().publish("terra.parallel.finish") os.environ["QISKIT_IN_PARALLEL"] = "FALSE" raise QiskitError("Keyboard interrupt in parallel_map.") from error # Otherwise just reset parallel flag and error os.environ["QISKIT_IN_PARALLEL"] = "FALSE" raise error - Publisher().publish("terra.parallel.finish") os.environ["QISKIT_IN_PARALLEL"] = "FALSE" return results @@ -193,6 +182,4 @@ def _callback(_): for _, value in enumerate(values): result = task(value, *task_args, **task_kwargs) results.append(result) - _callback(0) - Publisher().publish("terra.parallel.finish") return results diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index a70ef9c2822b..00c0e4d390a9 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -184,7 +184,7 @@ def circuit_drawer( :include-source: from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit - from qiskit.tools.visualization import circuit_drawer + from qiskit.visualization import circuit_drawer q = QuantumRegister(1) c = ClassicalRegister(1) qc = QuantumCircuit(q, c) diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 71c8e2d7b43c..558e6637da1d 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -622,13 +622,12 @@ def _set_bit_reg_info(self, wire_map, qubits_dict, clbits_dict, glob_data): longest_wire_label_width = 0 glob_data["n_lines"] = 0 - initial_qbit = " |0>" if self._initial_state else "" + initial_qbit = r" $|0\rangle$" if self._initial_state else "" initial_cbit = " 0" if self._initial_state else "" idx = 0 pos = y_off = -len(self._qubits) + 1 for ii, wire in enumerate(wire_map): - # if it's a creg, register is the key and just load the index if isinstance(wire, ClassicalRegister): # If wire came from ControlFlowOp and not in clbits, don't draw it @@ -1185,7 +1184,6 @@ def _condition(self, node, node_data, wire_map, outer_circuit, cond_xy, glob_dat cond_pos = [] if isinstance(condition, expr.Expr): - # If fixing this, please update the docstrings of `QuantumCircuit.draw` and # `visualization.circuit_drawer` to remove warnings. diff --git a/qiskit/visualization/library.py b/qiskit/visualization/library.py new file mode 100644 index 000000000000..5832a475f8fd --- /dev/null +++ b/qiskit/visualization/library.py @@ -0,0 +1,37 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2018. +# +# 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. + +# pylint: disable=invalid-name,no-name-in-module,ungrouped-imports + +"""A circuit library visualization""" + +from qiskit import QuantumCircuit +from qiskit.utils import optionals as _optionals + + +@_optionals.HAS_MATPLOTLIB.require_in_call +def _generate_circuit_library_visualization(circuit: QuantumCircuit): + import matplotlib.pyplot as plt + + circuit = circuit.decompose() + ops = circuit.count_ops() + num_nl = circuit.num_nonlocal_gates() + _fig, (ax0, ax1) = plt.subplots(2, 1) + circuit.draw("mpl", ax=ax0) + ax1.axis("off") + ax1.grid(visible=None) + ax1.table( + [[circuit.name], [circuit.width()], [circuit.depth()], [sum(ops.values())], [num_nl]], + rowLabels=["Circuit Name", "Width", "Depth", "Total Gates", "Non-local Gates"], + ) + plt.tight_layout() + plt.show() diff --git a/releasenotes/notes/deprecation-passmanager-0.25-95eb9b45b517370a.yaml b/releasenotes/notes/deprecation-passmanager-0.25-95eb9b45b517370a.yaml new file mode 100644 index 000000000000..1383cafc1f26 --- /dev/null +++ b/releasenotes/notes/deprecation-passmanager-0.25-95eb9b45b517370a.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Removed deprecated function :func:`~qiskit.transpiler.preset_passmanagers.common.get_vf2_call_limit` + and its corresponding test. Instead, use + :func:`~qiskit.transpiler.preset_passmanagers.common.get_vf2_limits` diff --git a/releasenotes/notes/fix-delay-broadcast-e8762b01dfd7e94f.yaml b/releasenotes/notes/fix-delay-broadcast-e8762b01dfd7e94f.yaml new file mode 100644 index 000000000000..2facbc643c14 --- /dev/null +++ b/releasenotes/notes/fix-delay-broadcast-e8762b01dfd7e94f.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + The qubit-argument broadcasting of :meth:`.QuantumCircuit.delay` now correctly produces + individual :class:`~.circuit.Delay` instructions for each qubit, as intended. Previously, when + given certain iterables (such as :class:`set`\ s), it would instead silently produce an invalid + circuit that might fail in unusual locations. diff --git a/releasenotes/notes/lazy-testers-warn-on-import-errors-95a9bdaacc9c3d2b.yaml b/releasenotes/notes/lazy-testers-warn-on-import-errors-95a9bdaacc9c3d2b.yaml new file mode 100644 index 000000000000..0a3167cc1f66 --- /dev/null +++ b/releasenotes/notes/lazy-testers-warn-on-import-errors-95a9bdaacc9c3d2b.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + A new warning base class, :exc:`.QiskitWarning`, was added. While Qiskit will continue to use + built-in Python warnings (such as :exc:`DeprecationWarning`) when those are most appropriate, + for cases that are more specific to Qiskit, the warnings will be subclasses of :exc:`.QiskitWarning`. + - | + :exc:`.QPYLoadingDeprecatedFeatureWarning` is now a subclass of :exc:`.QiskitWarning`. + - | + The optional-functionality testers (:mod:`qiskit.utils.optionals`) will now distinguish an + optional dependency that was completely not found (a normal situation) with one that was found, + but triggered errors during its import. In the latter case, they will now issue an + :exc:`.OptionalDependencyImportWarning` telling you what happened, since it might indicate a + failed installation or an incompatible version. diff --git a/releasenotes/notes/remove-ibmq-4bb57a04991da9af.yaml b/releasenotes/notes/remove-ibmq-4bb57a04991da9af.yaml new file mode 100644 index 000000000000..831b55ea9fdf --- /dev/null +++ b/releasenotes/notes/remove-ibmq-4bb57a04991da9af.yaml @@ -0,0 +1,34 @@ +--- +upgrade: + - | + The deprecated ``qiskit.IBMQ`` object hase been removed. This alias object + was marked as deprecated in the 0.23.0 release. This alias object lazily + redirected attribute access to ``qiskit.providers.ibmq.IBMQ``. As the + ``qiskit-ibmq-provider`` package has now been retired and superseded by + ``qiskit-ibm-provider`` package which maintains its own namespace + maintaining this alias is no longer relevant. If you + were relying on the ``qiskit.IBMQ`` alias you should migrate your usage to + the ``qiskit-ibm-provider`` package, see the + `migration guide `__ + for more details. + - | + Removed the deprecated module ``qiskit.tools.jupyter`` which previously + included jupyter magics and widgets for interactively visualizing some data + from Qiskit. This module was deprecated in Qiskit 0.46.0. Most of this + functionality was directly tied to the legacy ``qiskit-ibmq-provider`` + package and was no longer valid so the module was removed. Similar + functionality is available from the ``qiskit_ibm_provider.jupyter`` module + in the `qiskit-ibm-provider __` package. + - | + Removed the deprecated module ``qiskit.tools.monitor`` which previously + included tools for tracking :class:`.JobV1` job instances, primarily from + the legacy ``qiskit-ibm-provider`` package. This module was marked as + deprecated in Qiskit 0.46.0. It is being removed because it was directly + tied to the legacy ``qiskit-ibm-provider`` package. + - | + Removed the deprecated import path ``qiskit.test.mock`` which previously was + used to redirect imports for the mock backends to their newer location in + the :mod:`qiskit.providers.fake_provider`. This module was marked as + deprecated in Qiskit 0.37.0. If you were using this module you should update + your imports from ``qiskit.test.mock`` to + :mod:`qiskit.providers.fake_provider` instead. diff --git a/releasenotes/notes/remove-tools-2d13fc5ec1f45336.yaml b/releasenotes/notes/remove-tools-2d13fc5ec1f45336.yaml new file mode 100644 index 000000000000..d5e2e611a576 --- /dev/null +++ b/releasenotes/notes/remove-tools-2d13fc5ec1f45336.yaml @@ -0,0 +1,18 @@ +--- +upgrade: + - | + The deprecated ``qiskit.tools.visualization`` module has removed. This module + was deprecated in the Qiskit 0.46.0 release. This module was a legacy redirect + from the original location of Qiskit's visualization module and was moved to + :mod:`qiskit.visualization` in Qiskit 0.8.0. If you're still using this path + you can just update your imports from ``qiskit.tools.visualization`` + to :mod:`qiskit.visualization`. + - | + The deprecated ``qiskit.tools.events`` module and the corresponding + ``qiskit.tools.progressbar`` utility it exposed has been removed. It was deprecated + in the Qiskit 0.46.0 release. This module's functionality was not widely used and + better covered by dedicated packages such as `tqdm `__. + - | + The ``qiskit.tools`` module has been removed. This module was deprecated in Qiskit 0.46.0. + All the contents from this module have been removed except for the ``qiskit.tools.parallel_map`` + function which now can be used from :func:`qiskit.utils.parallel_map` instead. diff --git a/releasenotes/notes/upgrade-pass-manager-98aa64edde67b5bb.yaml b/releasenotes/notes/upgrade-pass-manager-98aa64edde67b5bb.yaml new file mode 100644 index 000000000000..be8b0ce26206 --- /dev/null +++ b/releasenotes/notes/upgrade-pass-manager-98aa64edde67b5bb.yaml @@ -0,0 +1,79 @@ +--- +upgrade: + - | + A pattern for the pass piepline construction was upgraded. + The syntactic suger shown below for instantiation of flow controller was removed. + + .. code-block:: python + + from qiskit.transpiler import PassManager + + pm = PassManager() + pm.append(my_pass, condition=condition_callable, do_while=do_while_callable) + + Instead of using this keyword argument pattern, you should explicitly instantiate the + flow controller. + + .. code-block:: python + + from qiskit.passmanager.flow_controllers import ConditionalController, DoWhileController + + pm = PassManager() + pm.append( + ConditionalController( + DoWhileController(my_pass, do_while=do_while_callable), + condition=condition_callable, + ) + ) + + Note that you can manage the pecking order of controllers when you want to nest them, + which was not possible with keyword arguments. + You can also build the pipeline with the constructor of the pass manager like below + because there is no reason to call the append method now. + + .. code-block:: python + + pm = PassManager( + ConditionalController( + DoWhileController(my_pass, do_while=do_while_callable), + condition=condition_callable, + ) + ) + + - | + The append method of builtin flow controllers was removed. This includes + + * :meth:`.ConditionalController.append` + * :meth:`.DoWhileController.append` + * :meth:`.FlowControllerLinear.append` + + The task pipeline in a flow controller is frozen, and it must be passed + when the controller instance is created. + + - | + Removed methods :meth:`qiskit.transpiler.PassManager.passes` and + :meth:`qiskit.transpiler.StagedPassManager.passes` that generates + a representation of inset passes in the form of list of dictionary, + however, this format doesn't efficiently represent more complicated pass pipeline, + which may include conditional branching and nested conditions. + Instead of using this representation, please use following pattern + + .. code-block:: python + + pm = PassManager(...) + pm.to_flow_controller().tasks + + This directly returns a linearized base task instances in tuple format. + + - | + The max_iteration argument was removed from :meth:`qiskit.transpiler.PassManager.append` + and :meth:`qiskit.transpiler.PassManager.replace`. + + - | + The following legacy classes were removed from the pass manager and transpiler module. + + * :class:`qiskit.passmanager.flow_controllers.FlowController` + * :class:`qiskit.transpiler.fencedobjs.FencedObject` + * :class:`qiskit.transpiler.fencedobjs.FencedPropertySet` + * :class:`qiskit.transpiler.fencedobjs.FencedDAGCircuit` + * :class:`qiskit.transpiler.runningpassmanager.RunningPassManager` diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 214e423b5247..30c37c73435c 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -36,7 +36,7 @@ from qiskit.quantum_info import Operator from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeOurense -from qiskit.tools import parallel_map +from qiskit.utils import parallel_map def raise_if_parameter_table_invalid(circuit): diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 988c85c5a721..9a5476ac7b05 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -93,7 +93,7 @@ from qiskit.pulse import InstructionScheduleMap from qiskit.quantum_info import Operator, random_unitary from qiskit.test import QiskitTestCase, slow_test -from qiskit.tools import parallel +from qiskit.utils import parallel from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout diff --git a/test/python/tools/__init__.py b/test/python/primitives/containers/__init__.py similarity index 85% rename from test/python/tools/__init__.py rename to test/python/primitives/containers/__init__.py index 39afed863464..bdefdb61023f 100644 --- a/test/python/tools/__init__.py +++ b/test/python/primitives/containers/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2018. +# (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 @@ -10,4 +10,4 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Qiskit tools tests.""" +"""Tests for the data containers of primitives.""" diff --git a/test/python/primitives/containers/test_bindings_array.py b/test/python/primitives/containers/test_bindings_array.py new file mode 100644 index 000000000000..9f714f38e83a --- /dev/null +++ b/test/python/primitives/containers/test_bindings_array.py @@ -0,0 +1,393 @@ +# 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 BindingsArray""" + + +import numpy as np + +from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit +from qiskit.primitives import BindingsArray +from qiskit.test import QiskitTestCase + + +class BindingsArrayTestCase(QiskitTestCase): + """Test the BindingsArray class""" + + def setUp(self): + self.circuit = QuantumCircuit(5) + self.params = ParameterVector("a", 50) + param_iter = iter(self.params) + for _ in range(10): + for qubit in range(5): + self.circuit.sx(qubit) + self.circuit.rz(next(param_iter), qubit) + self.circuit.cx(0, 1) + self.circuit.cx(2, 3) + return super().setUp() + + def test_construction_failures(self): + """Test all the possible construction failures""" + with self.assertRaisesRegex(ValueError, "inconsistent with last dimension of"): + BindingsArray(kwvals={Parameter("a"): [0, 1]}, shape=()) + + with self.assertRaisesRegex(ValueError, r"Array with shape \(\) inconsistent with \(1,\)"): + BindingsArray(kwvals={Parameter("a"): 0}, shape=(1,)) + + with self.assertRaisesRegex(ValueError, "ambiguous"): + # could have shape (1,) or (1, 1) + BindingsArray(kwvals={Parameter("a"): [[1]]}) + + with self.assertRaisesRegex(ValueError, r"\(3, 5\) inconsistent with \(2,\)"): + BindingsArray(np.empty((3, 5)), shape=2) + + with self.assertRaisesRegex(ValueError, "ambiguous"): + # could have shape (2,) or () + BindingsArray([np.empty(2), np.empty(2)]) + + with self.assertRaisesRegex(ValueError, "Could not find any consistent shape"): + BindingsArray([np.empty((5, 8, 3)), np.empty((4, 7, 2))]) + + with self.assertRaisesRegex(ValueError, "inconsistent with last dimension of"): + BindingsArray( + vals=np.empty((5, 10)), + kwvals={(Parameter("a"), Parameter("b")): np.empty((5, 10, 3))}, + ) + + def test_bind_at_idx(self): + """Test binding at a specified index""" + vals = np.linspace(0, 1, 1000).reshape((5, 4, 50)) + expected_circuit = self.circuit.assign_parameters(vals[2, 3]) + + ba = BindingsArray(vals) + self.assertEqual(ba.bind(self.circuit, (2, 3)), expected_circuit) + + ba = BindingsArray([vals[:, :, :20], vals[:, :, 20:27], vals[:, :, 27:]]) + self.assertEqual(ba.bind(self.circuit, (2, 3)), expected_circuit) + + ba = BindingsArray(vals[:, :, :20], {tuple(self.params[20:]): vals[:, :, 20:]}) + self.assertEqual(ba.bind(self.circuit, (2, 3)), expected_circuit) + + order = np.arange(30, 50, dtype=int) + np.random.default_rng().shuffle(order) + ba = BindingsArray( + [vals[:, :, :20], vals[:, :, 20:25]], + { + tuple(self.params[25:30]): vals[:, :, 25:30], + tuple(self.params[i] for i in order): vals[:, :, order], + }, + ) + self.assertEqual(ba.bind(self.circuit, (2, 3)), expected_circuit) + + def test_bind_all(self): + """Test binding all possible values""" + # this test assumes bind_all() is implemented via bind_at_idx(), which we have already + # tested. so here, we just test that it gets the order right + vals = np.linspace(0, 1, 300).reshape((2, 3, 50)) + bound_circuits = BindingsArray(vals).bind_all(self.circuit) + self.assertIsInstance(bound_circuits, np.ndarray) + self.assertEqual(bound_circuits.shape, (2, 3)) + for idx in np.ndindex((2, 3)): + self.assertEqual(bound_circuits[idx], self.circuit.assign_parameters(vals[idx])) + + def test_properties(self): + """Test properties""" + with self.subTest("binding a list"): + vals = np.linspace(0, 1, 50).tolist() + ba = BindingsArray(vals) + self.assertEqual(ba.num_parameters, 50) + self.assertEqual(ba.ndim, 0) + self.assertEqual(ba.shape, ()) + self.assertEqual(ba.size, 1) + self.assertEqual(ba.kwvals, {}) + np.testing.assert_allclose(ba.vals, np.array(vals)[:, np.newaxis]) + + with self.subTest("binding a single array"): + vals = np.linspace(0, 1, 300).reshape((2, 3, 50)) + ba = BindingsArray(vals) + self.assertEqual(ba.num_parameters, 50) + self.assertEqual(ba.ndim, 2) + self.assertEqual(ba.shape, (2, 3)) + self.assertEqual(ba.size, 6) + self.assertEqual(ba.kwvals, {}) + np.testing.assert_allclose(ba.vals, vals.reshape((1, 2, 3, 50))) + + with self.subTest("binding multiple arrays"): + vals = np.linspace(0, 1, 300).reshape((2, 3, 50)) + ba = BindingsArray([vals[:, :, :20], vals[:, :, 20:]]) + self.assertEqual(ba.num_parameters, 50) + self.assertEqual(ba.ndim, 2) + self.assertEqual(ba.shape, (2, 3)) + self.assertEqual(ba.size, 6) + self.assertEqual(ba.kwvals, {}) + self.assertEqual(len(ba.vals), 2) + np.testing.assert_allclose(ba.vals[0], vals[:, :, :20]) + np.testing.assert_allclose(ba.vals[1], vals[:, :, 20:]) + + def test_ravel(self): + """Test ravel""" + vals = np.linspace(0, 1, 300).reshape((2, 3, 50)) + + ba = BindingsArray(vals) + flat = ba.ravel() + self.assertEqual(flat.num_parameters, 50) + self.assertEqual(flat.ndim, 1) + self.assertEqual(flat.shape, (6,)) + self.assertEqual(flat.size, 6) + self.assertEqual(flat.kwvals, {}) + flat_vals = vals.reshape(-1, 50) + np.testing.assert_allclose(flat.vals, flat_vals.reshape((1, 6, 50))) + + bound_circuits = list(flat.bind_all(self.circuit).reshape(6)) + self.assertEqual(len(bound_circuits), 6) + for i in range(6): + self.assertEqual(bound_circuits[i], self.circuit.assign_parameters(flat_vals[i])) + + def test_reshape(self): + """Test reshape""" + vals = np.linspace(0, 1, 300).reshape((2, 3, 50)) + + with self.subTest("reshape"): + ba = BindingsArray(vals) + reshape_ba = ba.reshape((3, 2)) + self.assertEqual(reshape_ba.num_parameters, 50) + self.assertEqual(reshape_ba.ndim, 2) + self.assertEqual(reshape_ba.shape, (3, 2)) + self.assertEqual(reshape_ba.size, 6) + self.assertEqual(reshape_ba.kwvals, {}) + reshape_vals = vals.reshape((3, 2, 50)) + np.testing.assert_allclose(reshape_ba.vals, reshape_vals.reshape((1, 3, 2, 50))) + + circuit = self.circuit + bound_circuits = reshape_ba.bind_all(circuit) + self.assertEqual(bound_circuits.shape, (3, 2)) + self.assertEqual(bound_circuits[0, 0], circuit.assign_parameters(reshape_vals[0, 0])) + self.assertEqual(bound_circuits[0, 1], circuit.assign_parameters(reshape_vals[0, 1])) + self.assertEqual(bound_circuits[1, 0], circuit.assign_parameters(reshape_vals[1, 0])) + self.assertEqual(bound_circuits[1, 1], circuit.assign_parameters(reshape_vals[1, 1])) + self.assertEqual(bound_circuits[2, 0], circuit.assign_parameters(reshape_vals[2, 0])) + self.assertEqual(bound_circuits[2, 1], circuit.assign_parameters(reshape_vals[2, 1])) + + with self.subTest("flatten"): + ba = BindingsArray(vals) + reshape_ba = ba.reshape(6) + self.assertEqual(reshape_ba.num_parameters, 50) + self.assertEqual(reshape_ba.ndim, 1) + self.assertEqual(reshape_ba.shape, (6,)) + self.assertEqual(reshape_ba.size, 6) + self.assertEqual(reshape_ba.kwvals, {}) + reshape_vals = vals.reshape(-1, 50) + np.testing.assert_allclose(reshape_ba.vals, reshape_vals.reshape((1, 6, 50))) + + bound_circuits = list(reshape_ba.bind_all(self.circuit)) + self.assertEqual(len(bound_circuits), 6) + for i in range(6): + self.assertEqual(bound_circuits[i], self.circuit.assign_parameters(reshape_vals[i])) + + def test_kwvals(self): + """Test constructor with kwvals""" + with self.subTest("binding a single value"): + vals = np.linspace(0, 1, 50) + kwvals = {self.params: vals} + ba = BindingsArray(kwvals=kwvals) + self.assertEqual(ba.num_parameters, 50) + self.assertEqual(ba.ndim, 0) + self.assertEqual(ba.shape, ()) + self.assertEqual(ba.size, 1) + self.assertEqual(ba.vals, []) + self.assertEqual(ba.kwvals, {tuple(param.name for param in self.params): vals}) + + bound_circuit = ba.bind(self.circuit, ()) + self.assertEqual(bound_circuit, self.circuit.assign_parameters(vals)) + + with self.subTest("binding an array"): + vals = np.linspace(0, 1, 300).reshape((2, 3, 50)) + kwvals = {self.params: vals} + ba = BindingsArray(kwvals=kwvals) + self.assertEqual(ba.num_parameters, 50) + self.assertEqual(ba.ndim, 2) + self.assertEqual(ba.shape, (2, 3)) + self.assertEqual(ba.size, 6) + self.assertEqual(ba.vals, []) + self.assertEqual(ba.kwvals, {tuple(param.name for param in self.params): vals}) + + bound_circuits = ba.bind_all(self.circuit) + self.assertEqual(bound_circuits.shape, (2, 3)) + self.assertEqual(bound_circuits[0, 0], self.circuit.assign_parameters(vals[0, 0])) + self.assertEqual(bound_circuits[0, 1], self.circuit.assign_parameters(vals[0, 1])) + self.assertEqual(bound_circuits[0, 2], self.circuit.assign_parameters(vals[0, 2])) + self.assertEqual(bound_circuits[1, 0], self.circuit.assign_parameters(vals[1, 0])) + self.assertEqual(bound_circuits[1, 1], self.circuit.assign_parameters(vals[1, 1])) + self.assertEqual(bound_circuits[1, 2], self.circuit.assign_parameters(vals[1, 2])) + + with self.subTest("binding a single param"): + vals = np.linspace(0, 1, 50) + kwvals = {self.params[0]: vals} + ba = BindingsArray(kwvals=kwvals) + self.assertEqual(ba.num_parameters, 1) + self.assertEqual(ba.ndim, 1) + self.assertEqual(ba.shape, (50,)) + self.assertEqual(ba.size, 50) + self.assertEqual(ba.vals, []) + self.assertEqual(list(ba.kwvals.keys()), [(self.params[0].name,)]) + np.testing.assert_allclose(list(ba.kwvals.values()), [vals[..., np.newaxis]]) + + def test_vals_kwvals(self): + """Test constructor with vals and kwvals""" + with self.subTest("binding a single value"): + vals = np.linspace(0, 1, 50) + kwvals = {tuple(self.params[20:]): vals[20:]} + ba = BindingsArray(vals=vals[:20], kwvals=kwvals) + self.assertEqual(ba.num_parameters, 50) + self.assertEqual(ba.ndim, 0) + self.assertEqual(ba.shape, ()) + self.assertEqual(ba.size, 1) + np.testing.assert_allclose(ba.vals, vals[np.newaxis, :20]) + self.assertEqual(ba.kwvals, {tuple(p.name for p in k): v for k, v in kwvals.items()}) + + bound_circuit = ba.bind(self.circuit, ()) + self.assertEqual(bound_circuit, self.circuit.assign_parameters(vals)) + + with self.subTest("binding an array"): + vals = np.linspace(0, 1, 300).reshape((2, 3, 50)) + kwvals = {tuple(self.params[20:]): vals[:, :, 20:]} + ba = BindingsArray(vals=vals[:, :, :20], kwvals=kwvals) + self.assertEqual(ba.num_parameters, 50) + self.assertEqual(ba.ndim, 2) + self.assertEqual(ba.shape, (2, 3)) + self.assertEqual(ba.size, 6) + np.testing.assert_allclose(ba.vals, vals[np.newaxis, :, :, :20]) + self.assertEqual(ba.kwvals, {tuple(p.name for p in k): v for k, v in kwvals.items()}) + + bound_circuits = ba.bind_all(self.circuit) + self.assertEqual(bound_circuits.shape, (2, 3)) + self.assertEqual(bound_circuits[0, 0], self.circuit.assign_parameters(vals[0, 0])) + self.assertEqual(bound_circuits[0, 1], self.circuit.assign_parameters(vals[0, 1])) + self.assertEqual(bound_circuits[0, 2], self.circuit.assign_parameters(vals[0, 2])) + self.assertEqual(bound_circuits[1, 0], self.circuit.assign_parameters(vals[1, 0])) + self.assertEqual(bound_circuits[1, 1], self.circuit.assign_parameters(vals[1, 1])) + self.assertEqual(bound_circuits[1, 2], self.circuit.assign_parameters(vals[1, 2])) + + with self.subTest("len(val) == 1 and len(kwvals) > 0"): + ba = BindingsArray( + vals=np.empty((5, 10)), + kwvals={(Parameter("a"), Parameter("b")): np.empty((5, 10, 2))}, + ) + self.assertEqual(ba.num_parameters, 3) + self.assertEqual(ba.ndim, 2) + self.assertEqual(ba.shape, (5, 10)) + self.assertEqual(ba.size, 50) + + def test_simple_kwvals(self): + """Test simple constructions of BindingsArrays using kwvals.""" + with self.subTest("Single number kwval 1"): + ba = BindingsArray(kwvals={Parameter("a"): 1.0}) + self.assertEqual(ba.shape, ()) + + with self.subTest("Single number kwval 1 with shape"): + ba = BindingsArray(kwvals={Parameter("a"): 1.0}, shape=()) + self.assertEqual(ba.shape, ()) + + with self.subTest("Single number kwval 1 ndarray"): + ba = BindingsArray(kwvals={Parameter("a"): np.array(1.0)}) + self.assertEqual(ba.shape, ()) + + with self.subTest("Single number kwval 2"): + ba = BindingsArray(kwvals={Parameter("a"): 1.0, Parameter("b"): 0.0}) + self.assertEqual(ba.shape, ()) + + with self.subTest("Empty kwval"): + ba = BindingsArray(kwvals={Parameter("a"): []}) + self.assertEqual(ba.shape, (0,)) + + with self.subTest("Single kwval"): + ba = BindingsArray(kwvals={Parameter("a"): [0.0]}) + self.assertEqual(ba.shape, (1,)) + + with self.subTest("Single kwval ndarray"): + ba = BindingsArray(kwvals={Parameter("a"): np.array([0.0])}) + self.assertEqual(ba.shape, (1,)) + + with self.subTest("Multi kwval"): + ba = BindingsArray(kwvals={Parameter("a"): [0.0, 1.0]}) + self.assertEqual(ba.shape, (2,)) + + with self.subTest("Multiple kwvals empty"): + ba = BindingsArray(kwvals={Parameter("a"): [], Parameter("b"): []}) + self.assertEqual(ba.shape, (0,)) + + with self.subTest("Multiple kwvals single"): + ba = BindingsArray(kwvals={Parameter("a"): [0.0], Parameter("b"): [1.0]}) + self.assertEqual(ba.shape, (1,)) + + with self.subTest("Multiple kwvals multi"): + ba = BindingsArray(kwvals={Parameter("a"): [0.0, 1.0], Parameter("b"): [1.0, 0.0]}) + self.assertEqual(ba.shape, (2,)) + + def test_empty(self): + """Test simple constructions of BindingsArrays with empty cases""" + with self.subTest("Empty 1"): + ba = BindingsArray() + self.assertEqual(ba.shape, ()) + + with self.subTest("Empty 2"): + ba = BindingsArray([], shape=()) + self.assertEqual(ba.shape, ()) + + with self.subTest("Empty 3"): + ba = BindingsArray([], {}, shape=()) + self.assertEqual(ba.shape, ()) + + with self.subTest("Empty 4"): + ba = BindingsArray(shape=()) + self.assertEqual(ba.shape, ()) + + with self.subTest("Empty 5"): + ba = BindingsArray(kwvals={}, shape=()) + self.assertEqual(ba.shape, ()) + + def test_simple_vals(self): + """Test simple constructions of BindingsArrays using vals.""" + with self.subTest("0-d vals"): + ba = BindingsArray([1, 2, 3]) + self.assertEqual(ba.shape, ()) + # ba.vals => [array([1]), array([2]), array([3])] + self.assertEqual(len(ba.vals), 3) + self.assertEqual(ba.vals[0], 1) + self.assertEqual(ba.vals[1], 2) + self.assertEqual(ba.vals[2], 3) + + with self.subTest("1-d vals"): + ba = BindingsArray([[1, 2, 3]]) + self.assertEqual(ba.shape, ()) + # ba.vals => [array([1, 2, 3])] + self.assertEqual(len(ba.vals), 1) + np.testing.assert_allclose(ba.vals[0], [1, 2, 3]) + + with self.subTest("1-d vals ndarray"): + ba = BindingsArray(np.array([1, 2, 3])) + self.assertEqual(ba.shape, ()) + # ba.vals => [array([1, 2, 3])] + self.assertEqual(len(ba.vals), 1) + np.testing.assert_allclose(ba.vals[0], [1, 2, 3]) + + with self.subTest("2-d vals"): + ba = BindingsArray([[[1, 2, 3]]]) + self.assertEqual(ba.shape, (1,)) + self.assertEqual(len(ba.vals), 1) + np.testing.assert_allclose(ba.vals[0], [[1, 2, 3]]) + + with self.subTest("2-d vals ndarray"): + ba = BindingsArray(np.array([[1, 2, 3]])) + self.assertEqual(ba.shape, (1,)) + self.assertEqual(len(ba.vals), 1) + np.testing.assert_allclose(ba.vals[0], [[1, 2, 3]]) diff --git a/test/python/primitives/containers/test_data_bin.py b/test/python/primitives/containers/test_data_bin.py new file mode 100644 index 000000000000..003f0a997fc1 --- /dev/null +++ b/test/python/primitives/containers/test_data_bin.py @@ -0,0 +1,64 @@ +# 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 DataBin.""" + + +import numpy as np +import numpy.typing as npt + +from qiskit.primitives.containers import make_data_bin +from qiskit.primitives.containers.data_bin import DataBin, DataBinMeta +from qiskit.test import QiskitTestCase + + +class DataBinTestCase(QiskitTestCase): + """Test the DataBin class.""" + + def test_make_databin(self): + """Test the make_databin() function.""" + data_bin_cls = make_data_bin( + [("alpha", npt.NDArray[np.uint16]), ("beta", np.ndarray)], shape=(10, 20) + ) + + self.assertTrue(issubclass(type(data_bin_cls), DataBinMeta)) + self.assertTrue(issubclass(data_bin_cls, DataBin)) + self.assertEqual(data_bin_cls._FIELDS, ("alpha", "beta")) + self.assertEqual(data_bin_cls._FIELD_TYPES, (npt.NDArray[np.uint16], np.ndarray)) + + alpha = np.empty((10, 20), dtype=np.uint16) + beta = np.empty((10, 20), dtype=int) + my_bin = data_bin_cls(alpha, beta) + self.assertTrue(np.all(my_bin.alpha == alpha)) + self.assertTrue(np.all(my_bin.beta == beta)) + self.assertTrue("alpha=" in str(my_bin)) + self.assertTrue(str(my_bin).startswith("DataBin<10,20>")) + + my_bin = data_bin_cls(beta=beta, alpha=alpha) + self.assertTrue(np.all(my_bin.alpha == alpha)) + self.assertTrue(np.all(my_bin.beta == beta)) + + def test_make_databin_no_shape(self): + """Test the make_databin() function with no shape.""" + data_bin_cls = make_data_bin([("alpha", dict), ("beta", int)]) + + self.assertTrue(issubclass(type(data_bin_cls), DataBinMeta)) + self.assertTrue(issubclass(data_bin_cls, DataBin)) + self.assertEqual(data_bin_cls._FIELDS, ("alpha", "beta")) + self.assertEqual(data_bin_cls._FIELD_TYPES, (dict, int)) + + my_bin = data_bin_cls({1: 2}, 5) + self.assertEqual(my_bin.alpha, {1: 2}) + self.assertEqual(my_bin.beta, 5) + self.assertTrue("alpha=" in str(my_bin)) + self.assertTrue(">" not in str(my_bin)) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py new file mode 100644 index 000000000000..f3b08cec9229 --- /dev/null +++ b/test/python/primitives/containers/test_observables_array.py @@ -0,0 +1,306 @@ +# 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 ObservablesArray""" + +import itertools as it + +import ddt +import numpy as np + +import qiskit.quantum_info as qi +from qiskit.primitives import ObservablesArray +from qiskit.test import QiskitTestCase + + +@ddt.ddt +class ObservablesArrayTestCase(QiskitTestCase): + """Test the ObservablesArray class""" + + @ddt.data(0, 1, 2) + def test_format_observable_str(self, num_qubits): + """Test format_observable for allowed basis str input""" + for chars in it.permutations(ObservablesArray.ALLOWED_BASIS, num_qubits): + label = "".join(chars) + obs = ObservablesArray.format_observable(label) + self.assertEqual(obs, {label: 1}) + + def test_format_observable_custom_basis(self): + """Test format_observable for custom allowed basis""" + + class PauliArray(ObservablesArray): + """Custom array allowing only Paulis, not projectors""" + + ALLOWED_BASIS = "IXYZ" + + with self.assertRaises(ValueError): + PauliArray.format_observable("0101") + for p in qi.pauli_basis(1): + obs = PauliArray.format_observable(p) + self.assertEqual(obs, {p.to_label(): 1}) + + @ddt.data("iXX", "012", "+/-") + def test_format_observable_invalid_str(self, basis): + """Test format_observable for Pauli input""" + with self.assertRaises(ValueError): + ObservablesArray.format_observable(basis) + + @ddt.data(1, 2, 3) + def test_format_observable_pauli(self, num_qubits): + """Test format_observable for Pauli input""" + for p in qi.pauli_basis(num_qubits): + obs = ObservablesArray.format_observable(p) + self.assertEqual(obs, {p.to_label(): 1}) + + @ddt.data(0, 1, 2, 3) + def test_format_observable_phased_pauli(self, phase): + """Test format_observable for phased Pauli input""" + pauli = qi.Pauli("IXYZ") + pauli.phase = phase + coeff = (-1j) ** phase + obs = ObservablesArray.format_observable(pauli) + self.assertIsInstance(obs, dict) + self.assertEqual(list(obs.keys()), ["IXYZ"]) + np.testing.assert_allclose( + list(obs.values()), [coeff], err_msg=f"Wrong value for Pauli {pauli}" + ) + + @ddt.data("+IXYZ", "-IXYZ", "iIXYZ", "+iIXYZ", "-IXYZ") + def test_format_observable_phased_pauli_str(self, pauli): + """Test format_observable for phased Pauli input""" + pauli = qi.Pauli(pauli) + coeff = (-1j) ** pauli.phase + obs = ObservablesArray.format_observable(pauli) + self.assertIsInstance(obs, dict) + self.assertEqual(list(obs.keys()), ["IXYZ"]) + np.testing.assert_allclose( + list(obs.values()), [coeff], err_msg=f"Wrong value for Pauli {pauli}" + ) + + def test_format_observable_phased_sparse_pauli_op(self): + """Test format_observable for SparsePauliOp input with phase paulis""" + op = qi.SparsePauliOp(["+I", "-X", "iY", "-iZ"], [1, 2, 3, 4]) + obs = ObservablesArray.format_observable(op) + self.assertIsInstance(obs, dict) + self.assertEqual(len(obs), 4) + self.assertEqual(sorted(obs.keys()), sorted(["I", "X", "Y", "Z"])) + np.testing.assert_allclose([obs[i] for i in ["I", "X", "Y", "Z"]], [1, -2, 3j, -4j]) + + def test_format_observable_zero_sparse_pauli_op(self): + """Test format_observable for SparsePauliOp input with zero val coeffs""" + op = qi.SparsePauliOp(["I", "X", "Y", "Z"], [0, 0, 0, 1]) + obs = ObservablesArray.format_observable(op) + self.assertIsInstance(obs, dict) + self.assertEqual(len(obs), 1) + self.assertEqual(sorted(obs.keys()), ["Z"]) + self.assertEqual(obs["Z"], 1) + + def test_format_observable_duplicate_sparse_pauli_op(self): + """Test format_observable for SparsePauliOp wiht duplicate paulis""" + op = qi.SparsePauliOp(["XX", "-XX", "iXX", "-iXX"], [2, 1, 3, 2]) + obs = ObservablesArray.format_observable(op) + self.assertIsInstance(obs, dict) + self.assertEqual(len(obs), 1) + self.assertEqual(list(obs.keys()), ["XX"]) + self.assertEqual(obs["XX"], 1 + 1j) + + def test_format_observable_pauli_mapping(self): + """Test format_observable for pauli-keyed Mapping input""" + mapping = dict(zip(qi.pauli_basis(1), range(1, 5))) + obs = ObservablesArray.format_observable(mapping) + target = {key.to_label(): val for key, val in mapping.items()} + self.assertEqual(obs, target) + + def test_format_invalid_mapping_qubits(self): + """Test an error is raised when different qubits in mapping keys""" + mapping = {"IX": 1, "XXX": 2} + with self.assertRaises(ValueError): + ObservablesArray.format_observable(mapping) + + def test_format_invalid_mapping_basis(self): + """Test an error is raised when keys contain invalid characters""" + mapping = {"XX": 1, "0Z": 2, "02": 3} + with self.assertRaises(ValueError): + ObservablesArray.format_observable(mapping) + + def test_init_nested_list_str(self): + """Test init with nested lists of str""" + obj = [["X", "Y", "Z"], ["0", "1", "+"]] + obs = ObservablesArray(obj) + self.assertEqual(obs.size, 6) + self.assertEqual(obs.shape, (2, 3)) + + def test_init_nested_list_sparse_pauli_op(self): + """Test init with nested lists of SparsePauliOp""" + obj = [[qi.SparsePauliOp(qi.random_pauli_list(2, 3)) for _ in range(3)] for _ in range(5)] + obs = ObservablesArray(obj) + self.assertEqual(obs.size, 15) + self.assertEqual(obs.shape, (5, 3)) + + def test_init_single_sparse_pauli_op(self): + """Test init with single SparsePauliOps""" + obj = qi.SparsePauliOp(qi.random_pauli_list(2, 3)) + obs = ObservablesArray(obj) + self.assertEqual(obs.size, 1) + self.assertEqual(obs.shape, ()) + + def test_init_pauli_list(self): + """Test init with PauliList""" + obs = ObservablesArray(qi.pauli_basis(2)) + self.assertEqual(obs.size, 16) + self.assertEqual(obs.shape, (16,)) + + def test_init_nested_pauli_list(self): + """Test init with nested PauliList""" + obj = [qi.random_pauli_list(2, 3) for _ in range(5)] + obs = ObservablesArray(obj) + self.assertEqual(obs.size, 15) + self.assertEqual(obs.shape, (5, 3)) + + def test_init_ragged_array(self): + """Test init with ragged input""" + obj = [["X", "Y"], ["X", "Y", "Z"]] + with self.assertRaises(ValueError): + ObservablesArray(obj) + + def test_init_validate_false(self): + """Test init validate kwarg""" + obj = [["A", "B", "C"], ["D", "E", "F"]] + obs = ObservablesArray(obj, validate=False) + self.assertEqual(obs.shape, (2, 3)) + self.assertEqual(obs.size, 6) + for i in range(2): + for j in range(3): + self.assertEqual(obs[i, j], obj[i][j]) + + def test_init_validate_true(self): + """Test init validate kwarg""" + obj = [["A", "B", "C"], ["D", "E", "F"]] + with self.assertRaises(ValueError): + ObservablesArray(obj, validate=True) + + @ddt.data(0, 1, 2, 3) + def test_size_and_shape_single(self, ndim): + """Test size and shape method for size=1 array""" + obs = {"XX": 1} + for _ in range(ndim): + obs = [obs] + arr = ObservablesArray(obs, validate=False) + self.assertEqual(arr.size, 1, msg="Incorrect ObservablesArray.size") + self.assertEqual(arr.shape, (1,) * ndim, msg="Incorrect ObservablesArray.shape") + + @ddt.data(0, 1, 2, 3) + def test_tolist_single(self, ndim): + """Test tolist method for size=1 array""" + obs = {"XX": 1} + for _ in range(ndim): + obs = [obs] + arr = ObservablesArray(obs, validate=False) + ls = arr.tolist() + self.assertEqual(ls, obs) + + @ddt.data(0, 1, 2, 3) + def test_array_single(self, ndim): + """Test __array__ method for size=1 array""" + obs = {"XX": 1} + for _ in range(ndim): + obs = [obs] + arr = ObservablesArray(obs, validate=False) + nparr = np.array(arr) + self.assertEqual(nparr.dtype, object) + self.assertEqual(nparr.shape, arr.shape) + self.assertEqual(nparr.size, arr.size) + self.assertTrue(np.all(nparr == np.array(obs))) + + @ddt.data(0, 1, 2, 3) + def test_getitem_single(self, ndim): + """Test __getitem__ method for size=1 array""" + base_obs = {"XX": 1} + obs = base_obs + for _ in range(ndim): + obs = [obs] + arr = ObservablesArray(obs, validate=False) + idx = ndim * (0,) + item = arr[idx] + self.assertEqual(item, base_obs) + + def test_tolist_1d(self): + """Test tolist method""" + obj = ["A", "B", "C", "D"] + obs = ObservablesArray(obj, validate=False) + self.assertEqual(obs.tolist(), obj) + + def test_tolist_2d(self): + """Test tolist method""" + obj = [["A", "B", "C"], ["D", "E", "F"]] + obs = ObservablesArray(obj, validate=False) + self.assertEqual(obs.tolist(), obj) + + def test_array_1d(self): + """Test __array__ dunder method""" + obj = np.array(["A", "B", "C", "D"], dtype=object) + obs = ObservablesArray(obj, validate=False) + self.assertTrue(np.all(np.array(obs) == obj)) + + def test_array_2d(self): + """Test __array__ dunder method""" + obj = np.array([["A", "B", "C"], ["D", "E", "F"]], dtype=object) + obs = ObservablesArray(obj, validate=False) + self.assertTrue(np.all(np.array(obs) == obj)) + + def test_getitem_1d(self): + """Test __getitem__ for 1D array""" + obj = np.array(["A", "B", "C", "D"], dtype=object) + obs = ObservablesArray(obj, validate=False) + for i in range(obj.size): + self.assertEqual(obs[i], obj[i]) + + def test_getitem_2d(self): + """Test __getitem__ for 2D array""" + obj = np.array([["A", "B", "C"], ["D", "E", "F"]], dtype=object) + obs = ObservablesArray(obj, validate=False) + for i in range(obj.shape[0]): + row = obs[i] + self.assertIsInstance(row, ObservablesArray) + self.assertEqual(row.shape, (3,)) + self.assertTrue(np.all(np.array(row) == obj[i])) + + def test_ravel(self): + """Test ravel method""" + bases_flat = qi.pauli_basis(2).to_labels() + bases = [bases_flat[4 * i : 4 * (i + 1)] for i in range(4)] + obs = ObservablesArray(bases) + flat = obs.ravel() + self.assertEqual(flat.ndim, 1) + self.assertEqual(flat.shape, (16,)) + self.assertEqual(flat.size, 16) + for ( + i, + label, + ) in enumerate(bases_flat): + self.assertEqual(flat[i], {label: 1}) + + def test_reshape(self): + """Test reshape method""" + bases = qi.pauli_basis(2) + labels = np.array(bases.to_labels(), dtype=object) + obs = ObservablesArray(qi.pauli_basis(2)) + + for shape in [(16,), (4, 4), (2, 4, 2), (2, 2, 2, 2), (1, 8, 1, 2)]: + with self.subTest(shape): + obs_rs = obs.reshape(shape) + self.assertEqual(obs_rs.shape, shape) + labels_rs = labels.reshape(shape) + for idx in np.ndindex(shape): + self.assertEqual( + obs_rs[idx], {labels_rs[idx]: 1}, msg=f"failed for shape {shape}" + ) diff --git a/test/python/primitives/containers/test_primitive_result.py b/test/python/primitives/containers/test_primitive_result.py new file mode 100644 index 000000000000..25b2f505b21f --- /dev/null +++ b/test/python/primitives/containers/test_primitive_result.py @@ -0,0 +1,44 @@ +# 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 PrimitiveResult.""" + +import numpy as np +import numpy.typing as npt + +from qiskit.primitives.containers import PrimitiveResult, PubResult, make_data_bin +from qiskit.test import QiskitTestCase + + +class PrimitiveResultCase(QiskitTestCase): + """Test the PrimitiveResult class.""" + + def test_primitive_result(self): + """Test the PrimitiveResult class.""" + data_bin_cls = make_data_bin( + [("alpha", npt.NDArray[np.uint16]), ("beta", np.ndarray)], shape=(10, 20) + ) + + alpha = np.empty((10, 20), dtype=np.uint16) + beta = np.empty((10, 20), dtype=int) + + pub_results = [ + PubResult(data_bin_cls(alpha, beta)), + PubResult(data_bin_cls(alpha, beta)), + ] + result = PrimitiveResult(pub_results, {1: 2}) + + self.assertTrue(result[0] is pub_results[0]) + self.assertTrue(result[1] is pub_results[1]) + self.assertTrue(list(result)[0] is pub_results[0]) + self.assertEqual(len(result), 2) diff --git a/test/python/primitives/containers/test_shape.py b/test/python/primitives/containers/test_shape.py new file mode 100644 index 000000000000..c9c315613e24 --- /dev/null +++ b/test/python/primitives/containers/test_shape.py @@ -0,0 +1,105 @@ +# 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 shape.py module""" + + +import numpy as np + +from qiskit.primitives.containers.shape import Shaped, ShapedMixin, array_coerce, shape_tuple +from qiskit.test import QiskitTestCase + + +class DummyShaped(ShapedMixin): + """Dummy ShapedMixin child for testing.""" + + def __init__(self, arr): + super().__init__() + self._shape = arr.shape + self._arr = arr + + def __getitem__(self, arg): + return self._arr[arg] + + +class ShapedTestCase(QiskitTestCase): + """Test the Shaped protocol class""" + + def test_ndarray_is_shaped(self): + """Test that ndarrays are shaped""" + self.assertTrue(isinstance(np.empty((1, 2, 3)), Shaped)) + + def test_mixin_is_shaped(self): + """Test that ShapedMixin is shaped""" + self.assertTrue(isinstance(DummyShaped(np.empty((1, 2, 3))), Shaped)) + + +class ShapedMixinTestCase(QiskitTestCase): + """Test the ShapedMixin class""" + + def test_shape(self): + """Test the shape attribute.""" + self.assertEqual(DummyShaped(np.empty((1, 2, 3))).shape, (1, 2, 3)) + self.assertEqual(DummyShaped(np.empty(())).shape, ()) + + def test_ndim(self): + """Test the ndim attribute.""" + self.assertEqual(DummyShaped(np.empty(())).ndim, 0) + self.assertEqual(DummyShaped(np.empty((1, 2, 3))).ndim, 3) + + def test_size(self): + """Test the size attribute.""" + self.assertEqual(DummyShaped(np.empty(())).size, 1) + self.assertEqual(DummyShaped(np.empty((0, 1))).size, 0) + self.assertEqual(DummyShaped(np.empty((1, 2, 3))).size, 6) + + def test_getitem(self): + """Missing docstring.""" + arr = np.arange(100).reshape(2, 5, 10) + np.testing.assert_allclose(DummyShaped(arr)[:, 0, :2], arr[:, 0, :2]) + + +class ArrayCoerceTestCase(QiskitTestCase): + """Test array_coerce() function.""" + + def test_shaped(self): + """Test that array_coerce() works with ShapedMixin objects.""" + sh = DummyShaped(np.empty((1, 2, 3))) + self.assertIs(sh, array_coerce(sh)) + + def test_ndarray(self): + """Test that array_coerce() works with ndarray objects.""" + sh = np.arange(100).reshape(5, 2, 2, 5) + np.testing.assert_allclose(sh, array_coerce(sh)) + + +class ShapeTupleTestCase(QiskitTestCase): + """Test shape_tuple() function.""" + + def test_int(self): + """Test shape_tuple() with int inputs.""" + self.assertEqual(shape_tuple(), ()) + self.assertEqual(shape_tuple(5), (5,)) + self.assertEqual(shape_tuple(5, 10), (5, 10)) + self.assertEqual(shape_tuple(1e2), (100,)) + + def test_nested(self): + """Test shape_tuple() with nested inputs.""" + self.assertEqual(shape_tuple(0, (), (1, (2, (3,)), (4, 5))), (0, 1, 2, 3, 4, 5)) + + def test_exceptions(self): + """Test shape_tuple() raises correctly.""" + with self.assertRaisesRegex(ValueError, "iterable or an integer"): + shape_tuple(None) + + with self.assertRaisesRegex(ValueError, "iterable or an integer"): + shape_tuple(1.5) diff --git a/test/python/tools/test_pubsub.py b/test/python/tools/test_pubsub.py deleted file mode 100644 index 673c8e5445d1..000000000000 --- a/test/python/tools/test_pubsub.py +++ /dev/null @@ -1,92 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Tests for qiskit/tools/events/_pubsub.py""" - -from qiskit.tools.events.pubsub import Publisher, Subscriber -from qiskit.test import QiskitTestCase - - -class DummySubscriber(Subscriber): - """Simulates a component behaving like a Subscriber""" - - def __del__(self): - self.clear() - - -class TestPubSub(QiskitTestCase): - """A class for testing Publisher/Subscriber functionality.""" - - def test_pusbsub(self): - """Test subscribing works""" - sub = DummySubscriber() - - def action_callback(test): - """Callback called when 'publisher.action` event occurs""" - test.assertTrue(True) - - sub.subscribe("publisher.action", action_callback) - Publisher().publish("publisher.action", self) - - def test_single_broker(self): - """Testing a single broker is instantiated no matter how many - Publishers or Subscribers we have""" - - publishers = [Publisher() for _ in range(10)] - subscribers = [DummySubscriber() for _ in range(10)] - - for pub, sub in zip(publishers, subscribers): - self.assertEqual(id(pub._broker), id(sub._broker)) - - def test_double_subscribe(self): - """Testing that we cannot subscribe the same callback to the same event""" - - def callback(): - """This should be ever called""" - pass - - sub = DummySubscriber() - sub2 = DummySubscriber() - - sub.subscribe("event", callback) - self.assertFalse(sub.subscribe("event", callback)) - self.assertFalse(sub2.subscribe("event", callback)) - - def test_unsubscribe_simple(self): - """Testing a simple unsubscribe works""" - sub = DummySubscriber() - - def callback(_who, test): - """This should have ever been called""" - test.fail("We shouldn't have reach this code!") - - sub.subscribe("publisher.action", callback) - sub.unsubscribe("publisher.action", callback) - Publisher().publish("publisher.action", self) - - def test_unsubscribe_multiple(self): - """Testing unsubscribe works with many other subscribed event works""" - - sub = DummySubscriber() - - def callback(test): - """This should have ever been called""" - test.fail("We shouldn't have reach this code!") - - def dummy_callback(_test): - """Just a dummy callback, it won't be executed""" - pass - - sub.subscribe("publisher.action", callback) - sub.subscribe("publisher.action", dummy_callback) - sub.unsubscribe("publisher.action", callback) - Publisher().publish("publisher.action", self) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 1e737abdcfc2..7faefc566848 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -19,7 +19,6 @@ import numpy as np -import qiskit from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister from qiskit.circuit import Qubit, Gate, ControlFlowOp, ForLoopOp from qiskit.compiler import transpile, assemble @@ -272,11 +271,6 @@ def counting_callback_func(pass_, dag, time, property_set, count): ) self.assertEqual(gates_in_basis_true_count + 1, collect_2q_blocks_count) - def test_get_vf2_call_limit_deprecated(self): - """Test that calling test_get_vf2_call_limit emits deprecation warning.""" - with self.assertWarns(DeprecationWarning): - qiskit.transpiler.preset_passmanagers.common.get_vf2_call_limit(optimization_level=3) - @ddt class TestTranspileLevels(QiskitTestCase): diff --git a/test/python/tools/test_parallel.py b/test/python/utils/test_parallel.py similarity index 97% rename from test/python/tools/test_parallel.py rename to test/python/utils/test_parallel.py index 27801c1f6175..1194150ce4d7 100644 --- a/test/python/tools/test_parallel.py +++ b/test/python/utils/test_parallel.py @@ -16,7 +16,7 @@ from unittest.mock import patch -from qiskit.tools.parallel import get_platform_parallel_default, parallel_map +from qiskit.utils.parallel import get_platform_parallel_default, parallel_map from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.pulse import Schedule from qiskit.test import QiskitTestCase diff --git a/test/visual/mpl/circuit/references/creg_initial_true.png b/test/visual/mpl/circuit/references/creg_initial_true.png index 0972c9af28a4..f80054c6ec4d 100644 Binary files a/test/visual/mpl/circuit/references/creg_initial_true.png and b/test/visual/mpl/circuit/references/creg_initial_true.png differ