diff --git a/qiskit/extensions/quantum_initializer/__init__.py b/qiskit/extensions/quantum_initializer/__init__.py index accfd7e8ae7a..6f81827cc81f 100644 --- a/qiskit/extensions/quantum_initializer/__init__.py +++ b/qiskit/extensions/quantum_initializer/__init__.py @@ -12,6 +12,42 @@ """Initialize qubit registers to desired arbitrary state.""" +# pylint: disable=wrong-import-position + +import os +import platform +import warnings + +import numpy as np + +# The PyPI-distributed versions of Numpy 1.25 and 1.26 were compiled for macOS x86_64 using a +# compiler that caused a bug in the complex-multiply ufunc when AVX2 extensions are enabled. This +# severely affects the `Isometry` definition code to the point of the returned definitions being +# entirely unsound, so we need to warn users. See: +# +# - https://github.com/Qiskit/qiskit/issues/10305 +# - https://github.com/numpy/numpy/issues/24000 +_KNOWN_AFFECTED_NUMPY_VERSIONS = ("1.25.0", "1.25.1", "1.25.2", "1.26.0") +_IS_BAD_NUMPY = ( + os.environ.get("QISKIT_CMUL_AVX2_GOOD_NUMPY", "0") != "1" + and platform.system() == "Darwin" + and platform.machine() == "x86_64" + and np.__version__ in _KNOWN_AFFECTED_NUMPY_VERSIONS + and np.core._multiarray_umath.__cpu_features__.get("AVX2", False) +) + + +def _warn_if_bad_numpy(usage): + if not _IS_BAD_NUMPY: + return + msg = ( + f"On Intel macOS, NumPy {np.__version__} from PyPI has a bug in the complex-multiplication" + f" ufunc that severely affects {usage}." + " See https://qisk.it/cmul-avx2-numpy-bug for work-around information." + ) + warnings.warn(msg, RuntimeWarning, stacklevel=3) + + from .squ import SingleQubitUnitary from .uc_pauli_rot import UCPauliRotGate from .ucrz import UCRZGate diff --git a/qiskit/extensions/quantum_initializer/uc.py b/qiskit/extensions/quantum_initializer/uc.py index d859ed4a96de..f702000449a3 100644 --- a/qiskit/extensions/quantum_initializer/uc.py +++ b/qiskit/extensions/quantum_initializer/uc.py @@ -47,6 +47,7 @@ from qiskit.circuit.exceptions import CircuitError from qiskit.exceptions import QiskitError from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer +from qiskit.extensions.quantum_initializer import _warn_if_bad_numpy _EPS = 1e-10 # global variable used to chop very small numbers to zero _DECOMPOSER1Q = OneQubitEulerDecomposer("U3") @@ -134,6 +135,8 @@ def _dec_ucg(self): up_to_diagonal=True, the circuit implements the gate up to a diagonal gate and the diagonal gate is also returned. """ + _warn_if_bad_numpy("synthesis of multiplexed gates") + diag = np.ones(2**self.num_qubits).tolist() q = QuantumRegister(self.num_qubits) q_controls = q[1:] diff --git a/releasenotes/notes/numpy-avx2-cmul-ucgate-6c8708fb30f42f3f.yaml b/releasenotes/notes/numpy-avx2-cmul-ucgate-6c8708fb30f42f3f.yaml new file mode 100644 index 000000000000..3c524c5f4876 --- /dev/null +++ b/releasenotes/notes/numpy-avx2-cmul-ucgate-6c8708fb30f42f3f.yaml @@ -0,0 +1,18 @@ +--- +issues: + - | + The PyPI versions of the NumPy 1.25 series and the current most recent release 1.26.0 are known + to have inconsistent behaviour with the complex `multiply` ufunc when running on Intel Macs with + AVX2 CPU extensions. This bug destabilises the synthesis of :class:`.UCGate`, which is used by + :class:`.Isometry` and :meth:`.UnitaryGate.control`, to the degree of it returning completely + invalid results. + + This bug only affects Intel Macs with AVX2 CPU extensions enabled with certain versions of NumPy. + If the bad code paths are used, a :exc:`RuntimeWarning` will be issued. If you are affected, + you can install an older version of NumPy (the 1.24 series is known good), or you can disable + NumPy's use of AVX2 extensions by setting the environment variable + `NPY_DISABLE_CPU_FEATURES=AVX2` while importing `numpy`. See + https://qisk.it/cmul-avx2-numpy-bug for more detail. + + This bug also affects all prior versions of Qiskit with :class:`.UCGate` if the bad combination + of OS, CPU and NumPy version are present, but this is the first version that issues a warning. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 248aabab7839..8bdff79a036c 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -78,6 +78,7 @@ import qiskit.circuit.library.standard_gates as allGates from qiskit.extensions import UnitaryGate from qiskit.circuit.library.standard_gates.multi_control_rotation_gates import _mcsu2_real_diagonal +from qiskit.extensions.quantum_initializer import _IS_BAD_NUMPY from .gate_utils import _get_free_params @@ -843,6 +844,7 @@ def test_controlled_unitary(self, num_ctrl_qubits): self.assertTrue(matrix_equal(cop_mat, test_op.data)) @data(1, 2, 3, 4, 5) + @unittest.skipIf(_IS_BAD_NUMPY, "known-bad OS+NumPy combination for Isometry") def test_controlled_random_unitary(self, num_ctrl_qubits): """Test the matrix data of an Operator based on a random UnitaryGate.""" num_target = 2 diff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py index bffd9c69bac3..ae19cf13331e 100644 --- a/test/python/circuit/test_isometry.py +++ b/test/python/circuit/test_isometry.py @@ -25,10 +25,12 @@ from qiskit.test import QiskitTestCase from qiskit.compiler import transpile from qiskit.quantum_info import Operator +from qiskit.extensions.quantum_initializer import _IS_BAD_NUMPY from qiskit.extensions.quantum_initializer.isometry import Isometry @ddt +@unittest.skipIf(_IS_BAD_NUMPY, "known-bad OS+NumPy combination for Isometry") class TestIsometry(QiskitTestCase): """Qiskit isometry tests.""" diff --git a/test/python/circuit/test_uc.py b/test/python/circuit/test_uc.py index 1f50b650623c..094b79265479 100644 --- a/test/python/circuit/test_uc.py +++ b/test/python/circuit/test_uc.py @@ -23,6 +23,7 @@ import numpy as np from scipy.linalg import block_diag +from qiskit.extensions.quantum_initializer import _IS_BAD_NUMPY from qiskit.extensions.quantum_initializer.uc import UCGate from qiskit import QuantumCircuit, QuantumRegister, BasicAer, execute @@ -37,6 +38,7 @@ @ddt +@unittest.skipIf(_IS_BAD_NUMPY, "known-bad OS+NumPy combination for UCGate") class TestUCGate(QiskitTestCase): """Qiskit UCGate tests.""" diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py index 8edbd77222b1..ecfcdd3a7fc8 100644 --- a/test/python/circuit/test_unitary.py +++ b/test/python/circuit/test_unitary.py @@ -13,10 +13,13 @@ """UnitaryGate tests""" import json +import unittest + import numpy from numpy.testing import assert_allclose import qiskit +from qiskit.extensions.quantum_initializer import _IS_BAD_NUMPY from qiskit.extensions.unitary import UnitaryGate from qiskit.test import QiskitTestCase from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit @@ -303,6 +306,7 @@ def test_unitary_decomposition_via_definition_2q(self): mat = numpy.array([[0, 0, 1, 0], [0, 0, 0, -1], [1, 0, 0, 0], [0, -1, 0, 0]]) self.assertTrue(numpy.allclose(Operator(UnitaryGate(mat).definition).data, mat)) + @unittest.skipIf(_IS_BAD_NUMPY, "known-bad OS+NumPy combination for Isometry") def test_unitary_control(self): """Test parameters of controlled - unitary.""" mat = numpy.array([[0, 1], [1, 0]])