Source code for qiskit_dynamics.array.array

# This code is part of Qiskit.
#
# (C) Copyright IBM 2020.
#
# 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,global-statement

"""Array Class"""

import warnings
import copy
from functools import wraps
from types import BuiltinMethodType, MethodType
from typing import Any, Optional, Union, Tuple, Set
from numbers import Number

import numpy
from numpy.lib.mixins import NDArrayOperatorsMixin

from qiskit_dynamics.dispatch.dispatch import Dispatch, asarray

__all__ = ["Array"]

_deprecation_raised = False


def _array_deprecation_warn():
    global _deprecation_raised
    if not _deprecation_raised:
        warnings.warn(
            """The array and dispatch submodules of Qiskit Dynamics have been deprecated as of
            version 0.5.0. The use of the Array class is no longer required to work with different
            array libraries in Qiskit Dynamics, and is broken in some cases. Refer to the user guide
            entry on using different array libraries with Qiskit Dynamics. Users can now work
            directly with the supported array type of their choice, without the need to wrap them to
            enable dispatching. The array and dispatch submodules will be removed in version 0.6.0.
            """,
            DeprecationWarning,
            stacklevel=3,
        )
        _deprecation_raised = True


[docs] class Array(NDArrayOperatorsMixin): """Qiskit array class. This class is deprecated as of version 0.5.0 and will be removed in version 0.6.0. This class provides a Numpy compatible wrapper to supported Python array libraries. Supported backends are ``"numpy"`` and ``"jax"``. """ def __init__( self, data: Any, dtype: Optional[Any] = None, order: Optional[str] = None, backend: Optional[str] = None, ): """Initialize an :class:`Array` container. Args: data: An ``array_like`` input. This can be an object of any type supported by the registered :math:`asarray` method for the specified backend. dtype: Optional. The dtype of the returned array. This value must be supported by the specified array backend. order: Optional. The array order. This value must be supported by the specified array backend. backend: A registered array backend name. If ``None`` the default array backend will be used. Raises: ValueError: If input cannot be converted to an :class:`Array`. """ _array_deprecation_warn() # Check if we can override setattr and # set _data and _backend directly if ( isinstance(data, numpy.ndarray) and _is_numpy_backend(backend) and _is_equivalent_numpy_array(data, dtype, order) ): self.__dict__["_data"] = data self.__dict__["_backend"] = "numpy" return if hasattr(data, "__qiskit_array__"): array = data.__qiskit_array__(dtype=dtype, backend=backend) if not isinstance(array, Array): raise ValueError("object __qiskit_array__ method is not producing an Array") self._data = array._data self._backend = array._backend if dtype or order or (backend and backend != self._backend): if backend is None: backend = self._backend else: self._backend = backend self._data = asarray(self._data, dtype=dtype, order=order, backend=backend) return # Standard init self._data = asarray(data, dtype=dtype, order=order, backend=backend) self._backend = backend if backend else Dispatch.backend(self._data, subclass=True) @property def data(self): """The wrapped array data object.""" return self._data @data.setter def data(self, value): self._data[:] = value @property def backend(self): """The backend of the wrapped array class.""" return self._backend @backend.setter def backend(self, value: str): Dispatch.validate_backend(value) self._data = asarray(self._data, backend=value) self._backend = value
[docs] @classmethod def set_default_backend(cls, backend: Union[str, None]): """Set the default array backend.""" _array_deprecation_warn() if backend is not None: Dispatch.validate_backend(backend) Dispatch.DEFAULT_BACKEND = backend
[docs] @classmethod def default_backend(cls) -> str: """Return the default array backend.""" _array_deprecation_warn() return Dispatch.DEFAULT_BACKEND
[docs] @classmethod def available_backends(cls) -> Set[str]: """Return a tuple of available array backends.""" _array_deprecation_warn() return Dispatch.REGISTERED_BACKENDS
def __repr__(self): prefix = "Array(" if self._backend == Dispatch.DEFAULT_BACKEND: suffix = ")" else: suffix = f"backend='{self._backend}')" return Dispatch.repr(self.backend)(self._data, prefix=prefix, suffix=suffix) def __copy__(self): """Return a shallow copy referencing the same wrapped data array""" return Array(self._data, backend=self.backend) def __deepcopy__(self, memo=None): """Return a deep copy with a copy of the wrapped data array""" return Array(copy.deepcopy(self._data), backend=self.backend) def __iter__(self): return iter(self._data) def __getstate__(self): return {"_data": self._data, "_backend": self._backend} def __setstate__(self, state): self._backend = state["_backend"] self._data = state["_data"] def __getitem__(self, key: str) -> Any: """Return value from wrapped array.""" return self._data[key] def __setitem__(self, key: str, value: Any): """Return value of wrapped array.""" self._data[key] = value def __setattr__(self, name: str, value: Any): """Set attribute of wrapped array.""" if name in ("_data", "data", "_backend", "backend"): super().__setattr__(name, value) else: setattr(self._data, name, value) def __getattr__(self, name: str) -> Any: """Get attribute of wrapped array and convert to an :class:`Array`.""" # Call attribute on inner array object attr = getattr(self._data, name) # If attribute is a function wrap the return values if isinstance(attr, (MethodType, BuiltinMethodType)): @wraps(attr) def wrapped_method(*args, **kwargs): return self._wrap(attr(*args, **kwargs)) return wrapped_method # If return object is a backend array wrap result return self._wrap(attr) def __qiskit_array__(self, dtype=None, backend=None): if (backend and backend != self.backend) or (dtype and dtype != self.data.dtype): return Array(self.data, dtype=dtype, backend=backend) return self def __array__(self, dtype=None) -> numpy.ndarray: if isinstance(self._data, numpy.ndarray) and (dtype is None or dtype == self._data.dtype): return self._data return numpy.asarray(self._data, dtype=dtype) def __len__(self) -> int: return len(self._data) def __str__(self) -> str: return str(self._data) def __bool__(self) -> bool: return bool(self._data) def __int__(self): """Convert size 1 array to an int.""" if numpy.size(self) != 1: raise TypeError("only size-1 Arrays can be converted to Python scalars.") return int(self._data) def __float__(self): """Convert size 1 array to a float.""" if numpy.size(self) != 1: raise TypeError("only size-1 Arrays can be converted to Python scalars.") return float(self._data) def __complex__(self): """Convert size 1 array to a complex.""" if numpy.size(self) != 1: raise TypeError("only size-1 Arrays can be converted to Python scalars.") return complex(self._data) @staticmethod def _wrap(obj: Union[Any, Tuple[Any]], backend: Optional[str] = None) -> Union[Any, Tuple[Any]]: """Wrap return array backend objects as :class:`Array` objects.""" if isinstance(obj, tuple): return tuple( Array(x, backend=backend) if isinstance(x, Dispatch.REGISTERED_TYPES) else x for x in obj ) if isinstance(obj, Dispatch.REGISTERED_TYPES): return Array(obj, backend=backend) return obj @classmethod def _unwrap(cls, obj): """Unwrap an Array or list of :class:`Array` objects.""" if isinstance(obj, Array): return obj._data if isinstance(obj, tuple): return tuple(cls._unwrap(i) for i in obj) if isinstance(obj, list): return list(cls._unwrap(i) for i in obj) return obj def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """Dispatcher for NumPy ufuncs to support the wrapped array backend.""" out = kwargs.get("out", tuple()) for i in inputs + out: # Only support operations with instances of REGISTERED_TYPES. # Use ArrayLike instead of type(self) for isinstance to # allow subclasses that don't override __array_ufunc__ to # handle ArrayLike objects. if not isinstance(i, Dispatch.REGISTERED_TYPES + (Array, Number)): return NotImplemented # Defer to the implementation of the ufunc on unwrapped values. inputs = self._unwrap(inputs) if out: kwargs["out"] = self._unwrap(out) # Get implementation for backend backend = self.backend dispatch_func = Dispatch.array_ufunc(backend, ufunc, method) if dispatch_func == NotImplemented: return NotImplemented result = dispatch_func(*inputs, **kwargs) # Not sure what this case from NumPy docs is? if method == "at": return None # Wrap array results back into Array objects return self._wrap(result, backend=self.backend) def __array_function__(self, func, types, args, kwargs): """Dispatcher for NumPy array function to support the wrapped :class:`Array` backend.""" if not all(issubclass(t, (Array,) + Dispatch.REGISTERED_TYPES) for t in types): return NotImplemented # Unwrap function Array arguments args = self._unwrap(args) out = kwargs.get("out", tuple()) if out: kwargs["out"] = self._unwrap(out) # Get implementation for backend backend = self.backend dispatch_func = Dispatch.array_function(backend, func) if dispatch_func == NotImplemented: return NotImplemented result = dispatch_func(*args, **kwargs) return self._wrap(result, backend=self.backend)
def _is_numpy_backend(backend: Optional[str] = None): return backend == "numpy" or (not backend and Dispatch.DEFAULT_BACKEND == "numpy") def _is_equivalent_numpy_array(data: Any, dtype: Optional[Any] = None, order: Optional[str] = None): return (not dtype or dtype == data.dtype) and ( not order or (order == "C" and data.flags["C_CONTIGUOUS"]) or (order == "F" and data.flags["F_CONTIGUOUS"]) )