Skip to content

Commit

Permalink
Pennylane is compatible with numpy 2.0 (#6061)
Browse files Browse the repository at this point in the history
**Context:**

We want to make Pennylane compatible with Numpy 2.0. After several
discussions, we decided to test NumPy 2.0 on the CI by default in every
PR (testing both Python versions would have been to slow). Some jobs
still downgrade automatically to Numpy 1.x, since some interfaces (such
as Tensorflow) still do not support NumPy 2.0.

**Description of the Change:**

We can distinguish the changes into 3 main categories:

*Changes to workflows*

- None in the final version

*Changes to requirements and setup files*

- Unpin the Numpy version in `setup.py` (now we also allow Numpy 2.0).
- Update `requirements-ci.txt` to include Scipy 1.13 (this adds support
for Numpy 2.0).
- Pin Numpy in `requirements-ci.txt` to 2.0. 

*Changes to the source code*
- Change `np.NaN` to `np.nan`.
- Use legacy printing representation in tests, contrary to the new numpy
representation of scalars, e.g. np.float64(3.0) rather than just 3.0.
- Update probabilities warning to be case insensitive and check for a
partial match, since this warning was changed in Numpy 2.0.
- Check the datatype of np.exp from the Global phase only for Numpy 1.x,
since this gets promoted to complex128 in Numpy 2.x.
https://numpy.org/neps/nep-0050-scalar-promotion.html#schema-of-the-new-proposed-promotion-rules.

**Benefits:**

Make Pennylane compatible with Numpy 2.0.

**Possible Drawbacks:**

- We need to create a separate workflow to keep testing PennyLane with
NumPy 1.x, since we still want to maintain compatibility with previous
NumPy versions. This will be done in a separate PR.

- We are not testing Numpy 2.x for the interfaces that implicitly
require Numpy 1.x. These currently seem to be `tensorflow` and
`openfermionpyscf` (notice that `tensorflow` is required in some code
sections like qcut). In particular, `openfermionpyscf` causes an error:
```
AttributeError: np.string_ was removed in the NumPy 2.0 release. Use np.bytes_ instead.
``` 
in the qchem tests. The attribute `np.string_` is not used in the PL
source code, so it is a problem with the package itself.

[sc-61399]
[sc-66548]

---------

Co-authored-by: PietropaoloFrisoni <pietropaolo.frisoni@xanadu.ai>
Co-authored-by: Pietropaolo Frisoni <pietropfrisoni@gmail.com>
  • Loading branch information
3 people authored and mudit2812 committed Sep 18, 2024
1 parent 738b2c0 commit a049ffb
Show file tree
Hide file tree
Showing 17 changed files with 88 additions and 60 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/install_deps/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ inputs:
jax_version:
description: The version of JAX to install for any job that requires JAX
required: false
default: 0.4.23
default: '0.4.23'
install_tensorflow:
description: Indicate if TensorFlow should be installed or not
required: false
default: 'true'
tensorflow_version:
description: The version of TensorFlow to install for any job that requires TensorFlow
required: false
default: 2.16.0
default: '2.16.0'
install_pytorch:
description: Indicate if PyTorch should be installed or not
required: false
default: 'true'
pytorch_version:
description: The version of PyTorch to install for any job that requires PyTorch
required: false
default: 2.3.0
default: '2.3.0'
install_pennylane_lightning_master:
description: Indicate if PennyLane-Lightning should be installed from the master branch
required: false
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ config.toml
qml_debug.log
datasets/*
.benchmarks/*
*.h5
*.hdf5
5 changes: 5 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

<h3>Improvements 🛠</h3>

* PennyLane is now compatible with NumPy 2.0.
[(#6061)](https://github.com/PennyLaneAI/pennylane/pull/6061)

* `qml.qchem.excitations` now optionally returns fermionic operators.
[(#6171)](https://github.com/PennyLaneAI/pennylane/pull/6171)

Expand Down Expand Up @@ -133,6 +136,8 @@ Guillermo Alonso,
Utkarsh Azad,
Isaac De Vlugt,
Lillian M. A. Frederiksen,
Pietropaolo Frisoni,
Emiliano Godinez,
Christina Lee,
William Maxwell,
Lee J. O'Riordan,
Expand Down
5 changes: 3 additions & 2 deletions pennylane/numpy/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@
it works with the PennyLane :class:`~.tensor` class.
"""

from autograd.numpy import random as _random
# isort: skip_file
from numpy import __version__ as np_version
from numpy.random import MT19937, PCG64, SFC64, Philox # pylint: disable=unused-import
from autograd.numpy import random as _random
from packaging.specifiers import SpecifierSet
from packaging.version import Version

from .wrapper import tensor_wrapper, wrap_arrays

wrap_arrays(_random.__dict__, globals())


if Version(np_version) in SpecifierSet(">=0.17.0"):

# pylint: disable=too-few-public-methods
# pylint: disable=missing-class-docstring
class Generator(_random.Generator):
Expand Down
2 changes: 1 addition & 1 deletion requirements-ci.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
numpy
scipy<1.13.0
scipy<=1.13.0
cvxpy
cvxopt
networkx
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
version = f.readlines()[-1].split()[-1].strip("\"'")

requirements = [
"numpy<2.0",
"numpy<=2.0",
"scipy",
"networkx",
"rustworkx>=0.14.0",
Expand Down
11 changes: 8 additions & 3 deletions tests/data/attributes/operator/test_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def test_value_init(self, attribute_cls, op_in):
"""Test that a DatasetOperator can be value-initialized
from an operator, and that the deserialized operator
is equivalent."""

if not qml.operation.active_new_opmath() and isinstance(op_in, qml.ops.LinearCombination):
op_in = qml.operation.convert_to_legacy_H(op_in)

Expand All @@ -183,7 +184,8 @@ def test_value_init(self, attribute_cls, op_in):
assert dset_op.info["py_type"] == get_type_str(type(op_in))

op_out = dset_op.get_value()
assert repr(op_out) == repr(op_in)
with np.printoptions(legacy="1.21"):
assert repr(op_out) == repr(op_in)
assert op_in.data == op_out.data

@pytest.mark.parametrize(
Expand All @@ -199,6 +201,7 @@ def test_bind_init(self, attribute_cls, op_in):
"""Test that a DatasetOperator can be bind-initialized
from an operator, and that the deserialized operator
is equivalent."""

if not qml.operation.active_new_opmath() and isinstance(op_in, qml.ops.LinearCombination):
op_in = qml.operation.convert_to_legacy_H(op_in)

Expand All @@ -210,10 +213,12 @@ def test_bind_init(self, attribute_cls, op_in):
assert dset_op.info["py_type"] == get_type_str(type(op_in))

op_out = dset_op.get_value()
assert repr(op_out) == repr(op_in)
with np.printoptions(legacy="1.21"):
assert repr(op_out) == repr(op_in)
assert op_in.data == op_out.data
assert op_in.wires == op_out.wires
assert repr(op_in) == repr(op_out)
with np.printoptions(legacy="1.21"):
assert repr(op_in) == repr(op_out)


@pytest.mark.parametrize("attribute_cls", [DatasetOperator, DatasetPyTree])
Expand Down
10 changes: 7 additions & 3 deletions tests/data/attributes/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Tests for the ``DatasetDict`` attribute type.
"""

import numpy as np
import pytest

from pennylane.data.attributes import DatasetDict
Expand Down Expand Up @@ -45,7 +46,8 @@ def test_value_init(self, value):
assert dset_dict.info.py_type == "dict"
assert dset_dict.bind.keys() == value.keys()
assert len(dset_dict) == len(value)
assert repr(value) == repr(dset_dict)
with np.printoptions(legacy="1.21"):
assert repr(value) == repr(dset_dict)

@pytest.mark.parametrize(
"value", [{"a": 1, "b": 2}, {}, {"a": 1, "b": {"x": "y", "z": [1, 2]}}]
Expand Down Expand Up @@ -93,7 +95,8 @@ def test_copy(self, value):

assert builtin_dict.keys() == value.keys()
assert len(builtin_dict) == len(value)
assert repr(builtin_dict) == repr(value)
with np.printoptions(legacy="1.21"):
assert repr(builtin_dict) == repr(value)

@pytest.mark.parametrize(
"value", [{"a": 1, "b": 2}, {}, {"a": 1, "b": {"x": "y", "z": [1, 2]}}]
Expand Down Expand Up @@ -121,4 +124,5 @@ def test_equality_same_length(self):
)
def test_string_conversion(self, value):
dset_dict = DatasetDict(value)
assert str(dset_dict) == str(value)
with np.printoptions(legacy="1.21"):
assert str(dset_dict) == str(value)
12 changes: 9 additions & 3 deletions tests/data/attributes/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from itertools import combinations

import numpy as np
import pytest

from pennylane.data import DatasetList
Expand Down Expand Up @@ -56,8 +57,9 @@ def test_value_init(self, input_type, value):

lst = DatasetList(input_type(value))
assert lst == value
assert repr(lst) == repr(value)
assert len(lst) == len(value)
with np.printoptions(legacy="1.21"):
assert repr(lst) == repr(value)

@pytest.mark.parametrize("input_type", (list, tuple))
@pytest.mark.parametrize("value", [[], [1], [1, 2, 3], ["a", "b", "c"], [{"a": 1}]])
Expand Down Expand Up @@ -148,12 +150,14 @@ def test_setitem_out_of_range(self, index):
@pytest.mark.parametrize("value", [[], [1], [1, 2, 3], ["a", "b", "c"], [{"a": 1}]])
def test_copy(self, input_type, value):
"""Test that a `DatasetList` can be copied."""

ds = DatasetList(input_type(value))
ds_copy = ds.copy()

assert ds_copy == value
assert repr(ds_copy) == repr(value)
assert len(ds_copy) == len(value)
with np.printoptions(legacy="1.21"):
assert repr(ds_copy) == repr(value)

@pytest.mark.parametrize("input_type", (list, tuple))
@pytest.mark.parametrize("value", [[], [1], [1, 2, 3], ["a", "b", "c"], [{"a": 1}]])
Expand All @@ -169,8 +173,10 @@ def test_equality(self, input_type, value):
@pytest.mark.parametrize("value", [[], [1], [1, 2, 3], ["a", "b", "c"], [{"a": 1}]])
def test_string_conversion(self, value):
"""Test that a `DatasetList` is converted to a string correctly."""

dset_dict = DatasetList(value)
assert str(dset_dict) == str(value)
with np.printoptions(legacy="1.21"):
assert str(dset_dict) == str(value)

@pytest.mark.parametrize("value", [[1], [1, 2, 3], ["a", "b", "c"], [{"a": 1}]])
def test_deleting_elements(self, value):
Expand Down
4 changes: 2 additions & 2 deletions tests/data/base/test_attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ def test_bind_init_from_other_bind(self):
)
def test_repr(self, val, attribute_type):
"""Test that __repr__ has the expected format."""

assert repr(attribute(val)) == f"{attribute_type.__name__}({repr(val)})"
with np.printoptions(legacy="1.21"):
assert repr(attribute(val)) == f"{attribute_type.__name__}({repr(val)})"

@pytest.mark.parametrize(
"val",
Expand Down
8 changes: 4 additions & 4 deletions tests/devices/qubit/test_measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ class TestNaNMeasurements:
def test_nan_float_result(self, mp, interface):
"""Test that the result of circuits with 0 probability postselections is NaN with the
expected shape."""
state = qml.math.full((2, 2), np.NaN, like=interface)
state = qml.math.full((2, 2), np.nan, like=interface)
res = measure(mp, state, is_state_batched=False)

assert qml.math.ndim(res) == 0
Expand Down Expand Up @@ -339,7 +339,7 @@ def test_nan_float_result(self, mp, interface):
def test_nan_float_result_jax(self, mp, use_jit):
"""Test that the result of circuits with 0 probability postselections is NaN with the
expected shape."""
state = qml.math.full((2, 2), np.NaN, like="jax")
state = qml.math.full((2, 2), np.nan, like="jax")
if use_jit:
import jax

Expand All @@ -360,7 +360,7 @@ def test_nan_float_result_jax(self, mp, use_jit):
def test_nan_probs(self, mp, interface):
"""Test that the result of circuits with 0 probability postselections is NaN with the
expected shape."""
state = qml.math.full((2, 2), np.NaN, like=interface)
state = qml.math.full((2, 2), np.nan, like=interface)
res = measure(mp, state, is_state_batched=False)

assert qml.math.shape(res) == (2 ** len(mp.wires),)
Expand All @@ -375,7 +375,7 @@ def test_nan_probs(self, mp, interface):
def test_nan_probs_jax(self, mp, use_jit):
"""Test that the result of circuits with 0 probability postselections is NaN with the
expected shape."""
state = qml.math.full((2, 2), np.NaN, like="jax")
state = qml.math.full((2, 2), np.nan, like="jax")
if use_jit:
import jax

Expand Down
14 changes: 7 additions & 7 deletions tests/devices/qubit/test_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ def test_only_catch_nan_errors(self, shots):
mp = qml.expval(qml.PauliZ(0))
_shots = Shots(shots)

with pytest.raises(ValueError, match="probabilities do not sum to 1"):
with pytest.raises(ValueError, match=r"(?i)probabilities do not sum to 1"):
_ = measure_with_samples([mp], state, _shots)

@pytest.mark.all_interfaces
Expand Down Expand Up @@ -619,7 +619,7 @@ def test_only_catch_nan_errors(self, shots):
def test_nan_float_result(self, mp, interface, shots):
"""Test that the result of circuits with 0 probability postselections is NaN with the
expected shape."""
state = qml.math.full((2, 2), np.NaN, like=interface)
state = qml.math.full((2, 2), np.nan, like=interface)
res = measure_with_samples((mp,), state, _FlexShots(shots), is_state_batched=False)

if not isinstance(shots, list):
Expand All @@ -646,7 +646,7 @@ def test_nan_float_result(self, mp, interface, shots):
def test_nan_samples(self, mp, interface, shots):
"""Test that the result of circuits with 0 probability postselections is NaN with the
expected shape."""
state = qml.math.full((2, 2), np.NaN, like=interface)
state = qml.math.full((2, 2), np.nan, like=interface)
res = measure_with_samples((mp,), state, _FlexShots(shots), is_state_batched=False)

if not isinstance(shots, list):
Expand All @@ -672,7 +672,7 @@ def test_nan_samples(self, mp, interface, shots):
def test_nan_classical_shadows(self, interface, shots):
"""Test that classical_shadows returns an empty array when the state has
NaN values"""
state = qml.math.full((2, 2), np.NaN, like=interface)
state = qml.math.full((2, 2), np.nan, like=interface)
res = measure_with_samples(
(qml.classical_shadow([0]),), state, _FlexShots(shots), is_state_batched=False
)
Expand All @@ -699,7 +699,7 @@ def test_nan_classical_shadows(self, interface, shots):
def test_nan_shadow_expval(self, H, interface, shots):
"""Test that shadow_expval returns an empty array when the state has
NaN values"""
state = qml.math.full((2, 2), np.NaN, like=interface)
state = qml.math.full((2, 2), np.nan, like=interface)
res = measure_with_samples(
(qml.shadow_expval(H),), state, _FlexShots(shots), is_state_batched=False
)
Expand Down Expand Up @@ -757,7 +757,7 @@ def test_sample_state_renorm_error(self, interface):
"""Test that renormalization does not occur if the error is too large."""

state = qml.math.array(two_qubit_state_not_normalized, like=interface)
with pytest.raises(ValueError, match="probabilities do not sum to 1"):
with pytest.raises(ValueError, match=r"(?i)probabilities do not sum to 1"):
_ = sample_state(state, 10)

@pytest.mark.all_interfaces
Expand All @@ -775,7 +775,7 @@ def test_sample_batched_state_renorm_error(self, interface):
"""Test that renormalization does not occur if the error is too large."""

state = qml.math.array(batched_state_not_normalized, like=interface)
with pytest.raises(ValueError, match="probabilities do not sum to 1"):
with pytest.raises(ValueError, match=r"(?i)probabilities do not sum to 1"):
_ = sample_state(state, 10, is_state_batched=True)


Expand Down
2 changes: 1 addition & 1 deletion tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def test_only_catch_nan_errors(self, shots):
mp = qml.sample(wires=range(2))
_shots = Shots(shots)

with pytest.raises(ValueError, match="probabilities do not sum to 1"):
with pytest.raises(ValueError, match=r"(?i)probabilities do not sum to 1"):
_ = measure_with_samples(mp, state, _shots)

@pytest.mark.parametrize("mp", [qml.probs(0), qml.probs(op=qml.GellMann(0, 1))])
Expand Down
44 changes: 23 additions & 21 deletions tests/devices/test_default_qubit_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# pylint: disable=protected-access,cell-var-from-loop
import cmath
import math
from importlib.metadata import version

import pytest

Expand Down Expand Up @@ -628,7 +629,8 @@ def test_apply_global_phase(self, qubit_device_3_wires, tol, wire, input_state):
expected_output = np.array(input_state) * np.exp(-1j * phase)

assert np.allclose(qubit_device_3_wires._state, np.array(expected_output), atol=tol, rtol=0)
assert qubit_device_3_wires._state.dtype == qubit_device_3_wires.C_DTYPE
if version("numpy") < "2.0.0":
assert qubit_device_3_wires._state.dtype == qubit_device_3_wires.C_DTYPE

def test_apply_errors_qubit_state_vector(self, qubit_device_2_wires):
"""Test that apply fails for incorrect state preparation, and > 2 qubit gates"""
Expand All @@ -650,26 +652,26 @@ def test_apply_errors_qubit_state_vector(self, qubit_device_2_wires):
)

def test_apply_errors_basis_state(self, qubit_device_2_wires):

with pytest.raises(
ValueError, match=r"Basis state must only consist of 0s and 1s; got \[-0\.2, 4\.2\]"
):
qubit_device_2_wires.apply([qml.BasisState(np.array([-0.2, 4.2]), wires=[0, 1])])

with pytest.raises(
ValueError, match=r"State must be of length 1; got length 2 \(state=\[0 1\]\)\."
):
qubit_device_2_wires.apply([qml.BasisState(np.array([0, 1]), wires=[0])])

with pytest.raises(
qml.DeviceError,
match="Operation BasisState cannot be used after other Operations have already been applied "
"on a default.qubit.legacy device.",
):
qubit_device_2_wires.reset()
qubit_device_2_wires.apply(
[qml.RZ(0.5, wires=[0]), qml.BasisState(np.array([1, 1]), wires=[0, 1])]
)
with np.printoptions(legacy="1.21"):
with pytest.raises(
ValueError, match=r"Basis state must only consist of 0s and 1s; got \[-0\.2, 4\.2\]"
):
qubit_device_2_wires.apply([qml.BasisState(np.array([-0.2, 4.2]), wires=[0, 1])])

with pytest.raises(
ValueError, match=r"State must be of length 1; got length 2 \(state=\[0 1\]\)\."
):
qubit_device_2_wires.apply([qml.BasisState(np.array([0, 1]), wires=[0])])

with pytest.raises(
qml.DeviceError,
match="Operation BasisState cannot be used after other Operations have already been applied "
"on a default.qubit.legacy device.",
):
qubit_device_2_wires.reset()
qubit_device_2_wires.apply(
[qml.RZ(0.5, wires=[0]), qml.BasisState(np.array([1, 1]), wires=[0, 1])]
)


class TestExpval:
Expand Down
Loading

0 comments on commit a049ffb

Please sign in to comment.