diff --git a/filter_functions/basis.py b/filter_functions/basis.py index a69fefd..97f14ea 100644 --- a/filter_functions/basis.py +++ b/filter_functions/basis.py @@ -47,7 +47,6 @@ import numpy as np import opt_einsum as oe from numpy import linalg as nla -from numpy.core import ndarray from scipy import linalg as sla from sparse import COO @@ -56,7 +55,7 @@ __all__ = ['Basis', 'expand', 'ggm_expand', 'normalize'] -class Basis(ndarray): +class Basis(np.ndarray): r""" Class for operator bases. There are several ways to instantiate a Basis object: @@ -217,22 +216,26 @@ def __eq__(self, other: object) -> bool: # Not ndarray return np.equal(self, other) - return np.allclose(self.view(ndarray), other.view(ndarray), + return np.allclose(self.view(np.ndarray), other.view(np.ndarray), atol=self._atol, rtol=self._rtol) - def __contains__(self, item: ndarray) -> bool: + def __contains__(self, item: np.ndarray) -> bool: """Implement 'in' operator.""" - return any(np.isclose(item.view(ndarray), self.view(ndarray), + return any(np.isclose(item.view(np.ndarray), self.view(np.ndarray), rtol=self._rtol, atol=self._atol).all(axis=(1, 2))) - def __array_wrap__(self, out_arr, context=None): + def __array_wrap__(self, arr, context=None, return_scalar=False): """ Fixes problem that ufuncs return 0-d arrays instead of scalars. https://github.com/numpy/numpy/issues/5819#issue-72454838 """ - if out_arr.ndim: - return ndarray.__array_wrap__(self, out_arr, context) + try: + return super().__array_wrap__(arr, context, return_scalar=True) + except TypeError: + if arr.ndim: + # Numpy < 2 + return np.ndarray.__array_wrap__(self, arr, context) def _print_checks(self) -> None: """Print checks for debug purposes.""" @@ -265,7 +268,7 @@ def isorthonorm(self) -> bool: actual = U.conj() @ U.T target = np.identity(dim) atol = self._eps*(self.d**2)**3 - self._isorthonorm = np.allclose(actual.view(ndarray), target, + self._isorthonorm = np.allclose(actual.view(np.ndarray), target, atol=atol, rtol=self._rtol) return self._isorthonorm @@ -278,13 +281,16 @@ def istraceless(self) -> bool: if self._istraceless is None: trace = np.einsum('...jj', self) trace = util.remove_float_errors(trace, self.d**2) - nonzero = trace.nonzero() + nonzero = np.atleast_1d(trace).nonzero() if nonzero[0].size == 0: self._istraceless = True elif nonzero[0].size == 1: # Single element has nonzero trace, check if (proportional to) # identity - elem = self[nonzero][0].view(ndarray) if self.ndim == 3 else self.view(ndarray) + if self.ndim == 3: + elem = self[nonzero][0].view(np.ndarray) + else: + elem = self.view(np.ndarray) offdiag_nonzero = elem[~np.eye(self.d, dtype=bool)].nonzero() diag_equal = np.diag(elem) == elem[0, 0] if diag_equal.all() and not offdiag_nonzero[0].any(): @@ -597,7 +603,7 @@ def _full_from_partial(elems: Sequence, traceless: bool, labels: Sequence[str]) # sort Identity label to the front, default to first if not found # (should not happen since traceless checks that it is present) id_idx = next((i for i, elem in enumerate(elems) - if np.allclose(Id.view(ndarray), elem.view(ndarray), + if np.allclose(Id.view(np.ndarray), elem.view(np.ndarray), rtol=elems._rtol, atol=elems._atol)), 0) labels.insert(0, labels.pop(id_idx)) @@ -606,7 +612,7 @@ def _full_from_partial(elems: Sequence, traceless: bool, labels: Sequence[str]) return basis, labels -def _norm(b: Sequence) -> ndarray: +def _norm(b: Sequence) -> np.ndarray: """Frobenius norm with two singleton dimensions inserted at the end.""" b = np.asanyarray(b) norm = nla.norm(b, axis=(-1, -2)) @@ -633,8 +639,8 @@ def normalize(b: Basis) -> Basis: return (b/_norm(b)).squeeze().view(Basis) -def expand(M: Union[ndarray, Basis], basis: Union[ndarray, Basis], - normalized: bool = True, hermitian: bool = False, tidyup: bool = False) -> ndarray: +def expand(M: Union[np.ndarray, Basis], basis: Union[np.ndarray, Basis], + normalized: bool = True, hermitian: bool = False, tidyup: bool = False) -> np.ndarray: r""" Expand the array *M* in the basis given by *basis*. @@ -684,8 +690,8 @@ def cast(arr): return util.remove_float_errors(coefficients) if tidyup else coefficients -def ggm_expand(M: Union[ndarray, Basis], traceless: bool = False, - hermitian: bool = False) -> ndarray: +def ggm_expand(M: Union[np.ndarray, Basis], traceless: bool = False, + hermitian: bool = False) -> np.ndarray: r""" Expand the matrix *M* in a Generalized Gell-Mann basis [Bert08]_. This function makes use of the explicit construction prescription of @@ -767,7 +773,7 @@ def cast(arr): return coeffs.squeeze() if square else coeffs -def equivalent_pauli_basis_elements(idx: Union[Sequence[int], int], N: int) -> ndarray: +def equivalent_pauli_basis_elements(idx: Union[Sequence[int], int], N: int) -> np.ndarray: """ Get the indices of the equivalent (up to identities tensored to it) basis elements of Pauli bases of qubits at position idx in the total @@ -780,7 +786,7 @@ def equivalent_pauli_basis_elements(idx: Union[Sequence[int], int], N: int) -> n return elem_idx -def remap_pauli_basis_elements(order: Sequence[int], N: int) -> ndarray: +def remap_pauli_basis_elements(order: Sequence[int], N: int) -> np.ndarray: """ For a N-qubit Pauli basis, transpose the order of the subsystems and return the indices that permute the old basis to the new. diff --git a/setup.py b/setup.py index 2e85ad3..8b268a8 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def extract_version(version_file): 'bloch_sphere_visualization': ['qutip', 'matplotlib'], 'fancy_progressbar': ['ipynbname', 'jupyter'], 'doc': ['jupyter', 'nbsphinx', 'numpydoc', 'sphinx', 'sphinx_rtd_theme', - 'ipympl', 'qutip-qip', 'qutip-qtrl'], + 'ipympl', 'qutip-qip', 'qutip-qtrl', 'numpy<2'], 'tests': ['pytest>=4.6', 'pytest-cov', 'codecov']} extras_require['all'] = list({dep for deps in extras_require.values() for dep in deps}) diff --git a/tests/test_basis.py b/tests/test_basis.py index 9f5d3e4..edfacb1 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -24,12 +24,11 @@ from copy import copy from itertools import product +import filter_functions as ff import numpy as np import pytest -from sparse import COO - -import filter_functions as ff from filter_functions import util +from sparse import COO from tests import testutil from tests.testutil import rng @@ -152,9 +151,21 @@ def test_basis_properties(self): base._print_checks() - basis = util.paulis[1].view(ff.Basis) - self.assertTrue(basis.isorthonorm) - self.assertArrayEqual(basis.T, basis.view(np.ndarray).T) + # single element always considered orthonormal + orthonorm = rng.normal(size=(d, d)) + self.assertTrue(orthonorm.view(ff.Basis).isorthonorm) + + herm = testutil.rand_herm(d).squeeze() + self.assertTrue(herm.view(ff.Basis).isherm) + + herm[0, 1] += 1 + self.assertFalse(herm.view(ff.Basis).isherm) + + traceless = testutil.rand_herm_traceless(d).squeeze() + self.assertTrue(traceless.view(ff.Basis).istraceless) + + traceless[0, 0] += 1 + self.assertFalse(traceless.view(ff.Basis).istraceless) def test_transpose(self): arr = rng.normal(size=(2, 3, 3)) diff --git a/tests/test_gradient.py b/tests/test_gradient.py index 98499a1..9c060f9 100644 --- a/tests/test_gradient.py +++ b/tests/test_gradient.py @@ -134,7 +134,7 @@ def test_gradient_calculation_random_pulse(self): spectral_noise_density=gradient_testutil.one_over_f_noise, c_id=[f'c{i}' for i in range(len(u_ctrl))], n_coeffs_deriv=None ) - self.assertArrayAlmostEqual(ana_grad, fin_diff_grad, rtol=1e-6, atol=1e-8) + self.assertArrayAlmostEqual(ana_grad, fin_diff_grad, rtol=1e-5, atol=1e-7) def test_caching(self): """Make sure calculation works with or without cached intermediates."""