Skip to content

Commit 44b3146

Browse files
jakelishmanlevbishopraynelfss
authored
[0.46] Finalise support for Numpy 2.0 (#12189)
* Finalise support for Numpy 2.0 This commit brings the Qiskit test suite to a passing state (with all optionals installed) with Numpy 2.0.0b1, building on previous commits that handled much of the rest of the changing requirements: - gh-10890 - gh-10891 - gh-10892 - gh-10897 - gh-11023 Notably, this commit did not actually require a rebuild of Qiskit, despite us compiling against Numpy; it seems to happen that the C API stuff we use via `rust-numpy` (which loads the Numpy C extensions dynamically during module initialisation) hasn't changed. The main changes are: - adapting to the changed `copy=None` and `copy=False` semantics in `array` and `asarray`. - making sure all our implementers of `__array__` accept both `dtype` and `copy` arguments. Co-authored-by: Lev S. Bishop <18673315+levbishop@users.noreply.github.com> * Update `__array__` methods for Numpy 2.0 compatibility As of Numpy 2.0, implementers of `__array__` are expected and required to have a signature def __array__(self, dtype=None, copy=None): ... In Numpys before 2.0, the `copy` argument will never be passed, and the expected signature was def __array__(self, dtype=None): ... Because of this, we have latitude to set `copy` in our implementations to anything we like if we're running against Numpy 1.x, but we should default to `copy=None` if we're running against Numpy 2.0. The semantics of the `copy` argument to `np.array` changed in Numpy 2.0. Now, `copy=False` means "raise a `ValueError` if a copy is required" and `copy=None` means "copy only if required". In Numpy 1.x, `copy=False` meant "copy only if required". In _both_ Numpy 1.x and 2.0, `ndarray.astype` takes a `copy` argument, and in both, `copy=False` means "copy only if required". In Numpy 2.0 only, `np.asarray` gained a `copy` argument with the same semantics as the `np.array` copy argument from Numpy 2.0. Further, the semantics of the `__array__` method changed in Numpy 2.0, particularly around copying. Now, Numpy will assume that it can pass `copy=True` and the implementer will handle this. If `copy=False` is given and a copy or calculation is required, then the implementer is required to raise `ValueError`. We have a few places where the `__array__` method may (or always does) calculate the array, so in all these, we must forbid `copy=False`. With all this in mind: this PR sets up all our implementers of `__array__` to either default to `copy=None` if they will never actually need to _use_ the `copy` argument within themselves (except perhaps to test if it was set by Numpy 2.0 to `False`, as Numpy 1.x will never set it), or to a compatibility shim `_numpy_compat.COPY_ONLY_IF_NEEDED` if they do naturally want to use it with those semantics. The pattern def __array__(self, dtype=None, copy=_numpy_compat.COPY_ONLY_IF_NEEDED): dtype = self._array.dtype if dtype is None else dtype return np.array(self._array, dtype=dtype, copy=copy) using `array` instead of `asarray` lets us achieve all the desired behaviour between the interactions of `dtype` and `copy` in a way that is compatible with both Numpy 1.x and 2.x. --------- Co-authored-by: Lev S. Bishop <18673315+levbishop@users.noreply.github.com> Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com>
1 parent 86acb27 commit 44b3146

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+319
-154
lines changed

qiskit/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
)
4848

4949
import qiskit._accelerate
50+
import qiskit._numpy_compat
5051

5152
# Globally define compiled submodules. The normal import mechanism will not find compiled submodules
5253
# in _accelerate because it relies on file paths, but PyO3 generates only one shared library file.

qiskit/_numpy_compat.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2024.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
"""Compatiblity helpers for the Numpy 1.x to 2.0 transition."""
14+
15+
import re
16+
import typing
17+
import warnings
18+
19+
import numpy as np
20+
21+
# This version pattern is taken from the pypa packaging project:
22+
# https://github.com/pypa/packaging/blob/21.3/packaging/version.py#L223-L254 which is dual licensed
23+
# Apache 2.0 and BSD see the source for the original authors and other details.
24+
_VERSION_PATTERN = r"""
25+
v?
26+
(?:
27+
(?:(?P<epoch>[0-9]+)!)? # epoch
28+
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
29+
(?P<pre> # pre-release
30+
[-_\.]?
31+
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
32+
[-_\.]?
33+
(?P<pre_n>[0-9]+)?
34+
)?
35+
(?P<post> # post release
36+
(?:-(?P<post_n1>[0-9]+))
37+
|
38+
(?:
39+
[-_\.]?
40+
(?P<post_l>post|rev|r)
41+
[-_\.]?
42+
(?P<post_n2>[0-9]+)?
43+
)
44+
)?
45+
(?P<dev> # dev release
46+
[-_\.]?
47+
(?P<dev_l>dev)
48+
[-_\.]?
49+
(?P<dev_n>[0-9]+)?
50+
)?
51+
)
52+
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
53+
"""
54+
55+
VERSION = np.lib.NumpyVersion(np.__version__)
56+
VERSION_PARTS: typing.Tuple[int, ...]
57+
"""The numeric parts of the Numpy release version, e.g. ``(2, 0, 0)``. Does not include pre- or
58+
post-release markers (e.g. ``rc1``)."""
59+
if match := re.fullmatch(_VERSION_PATTERN, np.__version__, flags=re.VERBOSE | re.IGNORECASE):
60+
# Assuming Numpy won't ever introduce epochs, and we don't care about pre/post markers.
61+
VERSION_PARTS = tuple(int(x) for x in match["release"].split("."))
62+
else:
63+
# Just guess a version. We know all existing Numpys have good version strings, so the only way
64+
# this should trigger is from a new or a dev version.
65+
warnings.warn(
66+
f"Unrecognized version string for Numpy: '{np.__version__}'. Assuming Numpy 2.0.",
67+
RuntimeWarning,
68+
)
69+
VERSION_PARTS = (2, 0, 0)
70+
71+
COPY_ONLY_IF_NEEDED = None if VERSION_PARTS >= (2, 0, 0) else False
72+
"""The sentinel value given to ``np.array`` and ``np.ndarray.astype`` (etc) to indicate that a copy
73+
should be made only if required."""

qiskit/circuit/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@
382382
"""
383383

384384
from .exceptions import CircuitError
385+
from . import _utils
385386
from .quantumcircuit import QuantumCircuit
386387
from .classicalregister import ClassicalRegister, Clbit
387388
from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit

qiskit/circuit/_utils.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"""
1515

1616
import numpy
17+
18+
from qiskit import _numpy_compat
1719
from qiskit.exceptions import QiskitError
1820
from qiskit.circuit.exceptions import CircuitError
1921
from .parametervector import ParameterVectorElement
@@ -116,8 +118,9 @@ def with_gate_array(base_array):
116118
nonwritable = numpy.array(base_array, dtype=numpy.complex128)
117119
nonwritable.setflags(write=False)
118120

119-
def __array__(_self, dtype=None):
120-
return numpy.asarray(nonwritable, dtype=dtype)
121+
def __array__(_self, dtype=None, copy=_numpy_compat.COPY_ONLY_IF_NEEDED):
122+
dtype = nonwritable.dtype if dtype is None else dtype
123+
return numpy.array(nonwritable, dtype=dtype, copy=copy)
121124

122125
def decorator(cls):
123126
if hasattr(cls, "__array__"):
@@ -148,15 +151,21 @@ def matrix_for_control_state(state):
148151
if cached_states is None:
149152
nonwritables = [matrix_for_control_state(state) for state in range(2**num_ctrl_qubits)]
150153

151-
def __array__(self, dtype=None):
152-
return numpy.asarray(nonwritables[self.ctrl_state], dtype=dtype)
154+
def __array__(self, dtype=None, copy=_numpy_compat.COPY_ONLY_IF_NEEDED):
155+
arr = nonwritables[self.ctrl_state]
156+
dtype = arr.dtype if dtype is None else dtype
157+
return numpy.array(arr, dtype=dtype, copy=copy)
153158

154159
else:
155160
nonwritables = {state: matrix_for_control_state(state) for state in cached_states}
156161

157-
def __array__(self, dtype=None):
158-
if (out := nonwritables.get(self.ctrl_state)) is not None:
159-
return numpy.asarray(out, dtype=dtype)
162+
def __array__(self, dtype=None, copy=_numpy_compat.COPY_ONLY_IF_NEEDED):
163+
if (arr := nonwritables.get(self.ctrl_state)) is not None:
164+
dtype = arr.dtype if dtype is None else dtype
165+
return numpy.array(arr, dtype=dtype, copy=copy)
166+
167+
if copy is False and copy is not _numpy_compat.COPY_ONLY_IF_NEEDED:
168+
raise ValueError("could not produce matrix without calculation")
160169
return numpy.asarray(
161170
_compute_control_matrix(base, num_ctrl_qubits, self.ctrl_state), dtype=dtype
162171
)

qiskit/circuit/delay.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
from qiskit.circuit.exceptions import CircuitError
1818
from qiskit.circuit.instruction import Instruction
1919
from qiskit.circuit.gate import Gate
20+
from qiskit.circuit import _utils
2021
from qiskit.circuit.parameterexpression import ParameterExpression
2122

2223

24+
@_utils.with_gate_array(np.eye(2, dtype=complex))
2325
class Delay(Instruction):
2426
"""Do nothing and just delay/wait/idle for a specified duration."""
2527

@@ -49,10 +51,6 @@ def duration(self, duration):
4951
"""Set the duration of this delay."""
5052
self.params = [duration]
5153

52-
def __array__(self, dtype=None):
53-
"""Return the identity matrix."""
54-
return np.array([[1, 0], [0, 1]], dtype=dtype)
55-
5654
def to_matrix(self) -> np.ndarray:
5755
"""Return a Numpy.array for the unitary matrix. This has been
5856
added to enable simulation without making delay a full Gate type.

qiskit/circuit/library/generalized_gates/pauli.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,13 @@ def inverse(self):
6363
r"""Return inverted pauli gate (itself)."""
6464
return PauliGate(self.params[0]) # self-inverse
6565

66-
def __array__(self, dtype=None):
66+
def __array__(self, dtype=None, copy=None):
6767
"""Return a Numpy.array for the pauli gate.
6868
i.e. tensor product of the paulis"""
6969
# pylint: disable=cyclic-import
7070
from qiskit.quantum_info.operators import Pauli
7171

72-
return Pauli(self.params[0]).__array__(dtype=dtype)
72+
return Pauli(self.params[0]).__array__(dtype=dtype, copy=copy)
7373

7474
def validate_parameter(self, parameter):
7575
if isinstance(parameter, str):

qiskit/circuit/library/generalized_gates/permutation.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,11 @@ def __init__(
147147

148148
super().__init__(name="permutation", num_qubits=num_qubits, params=[pattern])
149149

150-
def __array__(self, dtype=None):
150+
def __array__(self, dtype=None, copy=None):
151151
"""Return a numpy.array for the Permutation gate."""
152+
if copy is False:
153+
raise ValueError("cannot produce matrix without calculation")
154+
152155
nq = len(self.pattern)
153156
mat = np.zeros((2**nq, 2**nq), dtype=dtype)
154157

qiskit/circuit/library/generalized_gates/unitary.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import typing
1818
import numpy
1919

20+
from qiskit import _numpy_compat
2021
from qiskit.circuit.gate import Gate
2122
from qiskit.circuit.controlledgate import ControlledGate
2223
from qiskit.circuit.quantumcircuit import QuantumCircuit
@@ -115,10 +116,10 @@ def __eq__(self, other):
115116
# up to global phase?
116117
return matrix_equal(self.params[0], other.params[0], ignore_phase=True)
117118

118-
def __array__(self, dtype=None):
119+
def __array__(self, dtype=None, copy=_numpy_compat.COPY_ONLY_IF_NEEDED):
119120
"""Return matrix for the unitary."""
120-
# pylint: disable=unused-argument
121-
return self.params[0]
121+
dtype = self.params[0].dtype if dtype is None else dtype
122+
return numpy.array(self.params[0], dtype=dtype, copy=copy)
122123

123124
def inverse(self):
124125
"""Return the adjoint of the unitary."""

qiskit/circuit/library/hamiltonian_gate.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from numbers import Number
2121
import numpy as np
2222

23+
from qiskit import _numpy_compat
2324
from qiskit.circuit.gate import Gate
2425
from qiskit.circuit.quantumcircuit import QuantumCircuit
2526
from qiskit.circuit.quantumregister import QuantumRegister
@@ -92,18 +93,22 @@ def __eq__(self, other):
9293
times_eq = self.params[1] == other.params[1]
9394
return operators_eq and times_eq
9495

95-
def __array__(self, dtype=None):
96+
def __array__(self, dtype=None, copy=None):
9697
"""Return matrix for the unitary."""
97-
# pylint: disable=unused-argument
9898
import scipy.linalg
9999

100+
if copy is False:
101+
raise ValueError("cannot produce matrix without calculation")
100102
try:
101-
return scipy.linalg.expm(-1j * self.params[0] * float(self.params[1]))
103+
time = float(self.params[1])
102104
except TypeError as ex:
103105
raise TypeError(
104106
"Unable to generate Unitary matrix for "
105107
"unbound t parameter {}".format(self.params[1])
106108
) from ex
109+
arr = scipy.linalg.expm(-1j * self.params[0] * time)
110+
dtype = complex if dtype is None else dtype
111+
return np.array(arr, dtype=dtype, copy=_numpy_compat.COPY_ONLY_IF_NEEDED)
107112

108113
def inverse(self):
109114
"""Return the adjoint of the unitary."""

qiskit/circuit/library/standard_gates/global_phase.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def inverse(self):
5757
"""
5858
return GlobalPhaseGate(-self.params[0])
5959

60-
def __array__(self, dtype=complex):
60+
def __array__(self, dtype=None, copy=None):
6161
"""Return a numpy.array for the global_phase gate."""
62+
if copy is False:
63+
raise ValueError("cannot produce matrix without calculation")
6264
theta = self.params[0]
63-
return numpy.array([[numpy.exp(1j * theta)]], dtype=dtype)
65+
return numpy.array([[numpy.exp(1j * theta)]], dtype=dtype or complex)

qiskit/circuit/library/standard_gates/p.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,10 @@ def inverse(self):
123123
r"""Return inverted Phase gate (:math:`Phase(\lambda)^{\dagger} = Phase(-\lambda)`)"""
124124
return PhaseGate(-self.params[0])
125125

126-
def __array__(self, dtype=None):
126+
def __array__(self, dtype=None, copy=None):
127127
"""Return a numpy.array for the Phase gate."""
128+
if copy is False:
129+
raise ValueError("cannot produce matrix without calculation")
128130
lam = float(self.params[0])
129131
return numpy.array([[1, 0], [0, exp(1j * lam)]], dtype=dtype)
130132

@@ -249,8 +251,10 @@ def inverse(self):
249251
r"""Return inverted CPhase gate (:math:`CPhase(\lambda)^{\dagger} = CPhase(-\lambda)`)"""
250252
return CPhaseGate(-self.params[0], ctrl_state=self.ctrl_state)
251253

252-
def __array__(self, dtype=None):
254+
def __array__(self, dtype=None, copy=None):
253255
"""Return a numpy.array for the CPhase gate."""
256+
if copy is False:
257+
raise ValueError("cannot produce matrix without calculation")
254258
eith = exp(1j * float(self.params[0]))
255259
if self.ctrl_state:
256260
return numpy.array(

qiskit/circuit/library/standard_gates/r.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@ def inverse(self):
8686
"""
8787
return RGate(-self.params[0], self.params[1])
8888

89-
def __array__(self, dtype=None):
89+
def __array__(self, dtype=None, copy=None):
9090
"""Return a numpy.array for the R gate."""
91+
if copy is False:
92+
raise ValueError("cannot produce matrix without calculation")
9193
theta, phi = float(self.params[0]), float(self.params[1])
9294
cos = math.cos(theta / 2)
9395
sin = math.sin(theta / 2)

qiskit/circuit/library/standard_gates/rx.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ def inverse(self):
102102
"""
103103
return RXGate(-self.params[0])
104104

105-
def __array__(self, dtype=None):
105+
def __array__(self, dtype=None, copy=None):
106106
"""Return a numpy.array for the RX gate."""
107+
if copy is False:
108+
raise ValueError("cannot produce matrix without calculation")
107109
cos = math.cos(self.params[0] / 2)
108110
sin = math.sin(self.params[0] / 2)
109111
return numpy.array([[cos, -1j * sin], [-1j * sin, cos]], dtype=dtype)
@@ -231,8 +233,10 @@ def inverse(self):
231233
"""Return inverse CRX gate (i.e. with the negative rotation angle)."""
232234
return CRXGate(-self.params[0], ctrl_state=self.ctrl_state)
233235

234-
def __array__(self, dtype=None):
236+
def __array__(self, dtype=None, copy=None):
235237
"""Return a numpy.array for the CRX gate."""
238+
if copy is False:
239+
raise ValueError("cannot produce matrix without calculation")
236240
half_theta = float(self.params[0]) / 2
237241
cos = math.cos(half_theta)
238242
isin = 1j * math.sin(half_theta)

qiskit/circuit/library/standard_gates/rxx.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ def inverse(self):
112112
"""Return inverse RXX gate (i.e. with the negative rotation angle)."""
113113
return RXXGate(-self.params[0])
114114

115-
def __array__(self, dtype=None):
115+
def __array__(self, dtype=None, copy=None):
116116
"""Return a Numpy.array for the RXX gate."""
117+
if copy is False:
118+
raise ValueError("cannot produce matrix without calculation")
117119
theta2 = float(self.params[0]) / 2
118120
cos = math.cos(theta2)
119121
isin = 1j * math.sin(theta2)

qiskit/circuit/library/standard_gates/ry.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ def inverse(self):
101101
"""
102102
return RYGate(-self.params[0])
103103

104-
def __array__(self, dtype=None):
104+
def __array__(self, dtype=None, copy=None):
105105
"""Return a numpy.array for the RY gate."""
106+
if copy is False:
107+
raise ValueError("cannot produce matrix without calculation")
106108
cos = math.cos(self.params[0] / 2)
107109
sin = math.sin(self.params[0] / 2)
108110
return numpy.array([[cos, -sin], [sin, cos]], dtype=dtype)
@@ -226,8 +228,10 @@ def inverse(self):
226228
"""Return inverse CRY gate (i.e. with the negative rotation angle)."""
227229
return CRYGate(-self.params[0], ctrl_state=self.ctrl_state)
228230

229-
def __array__(self, dtype=None):
231+
def __array__(self, dtype=None, copy=None):
230232
"""Return a numpy.array for the CRY gate."""
233+
if copy is False:
234+
raise ValueError("cannot produce matrix without calculation")
231235
half_theta = float(self.params[0]) / 2
232236
cos = math.cos(half_theta)
233237
sin = math.sin(half_theta)

qiskit/circuit/library/standard_gates/ryy.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ def inverse(self):
112112
"""Return inverse RYY gate (i.e. with the negative rotation angle)."""
113113
return RYYGate(-self.params[0])
114114

115-
def __array__(self, dtype=None):
115+
def __array__(self, dtype=None, copy=None):
116116
"""Return a numpy.array for the RYY gate."""
117+
if copy is False:
118+
raise ValueError("cannot produce matrix without calculation")
117119
theta = float(self.params[0])
118120
cos = math.cos(theta / 2)
119121
isin = 1j * math.sin(theta / 2)

qiskit/circuit/library/standard_gates/rz.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,12 @@ def inverse(self):
112112
"""
113113
return RZGate(-self.params[0])
114114

115-
def __array__(self, dtype=None):
115+
def __array__(self, dtype=None, copy=None):
116116
"""Return a numpy.array for the RZ gate."""
117117
import numpy as np
118118

119+
if copy is False:
120+
raise ValueError("cannot produce matrix without calculation")
119121
ilam2 = 0.5j * float(self.params[0])
120122
return np.array([[exp(-ilam2), 0], [0, exp(ilam2)]], dtype=dtype)
121123

@@ -244,10 +246,12 @@ def inverse(self):
244246
"""Return inverse CRZ gate (i.e. with the negative rotation angle)."""
245247
return CRZGate(-self.params[0], ctrl_state=self.ctrl_state)
246248

247-
def __array__(self, dtype=None):
249+
def __array__(self, dtype=None, copy=None):
248250
"""Return a numpy.array for the CRZ gate."""
249251
import numpy
250252

253+
if copy is False:
254+
raise ValueError("cannot produce matrix without calculation")
251255
arg = 1j * float(self.params[0]) / 2
252256
if self.ctrl_state:
253257
return numpy.array(

0 commit comments

Comments
 (0)