Repository: yt-project/unyt (language: python)

Issue:
TST: new failure with numpy 2.0 dev
### Description
Nightly tests started failing last night (see https://github.com/yt-project/unyt/actions/runs/7121865476)
The reason appears to be that `numpy.linalg.cross`, which was added in https://github.com/numpy/numpy/pull/25145, isn't handled yet as an array function (NEP18).
It also appears that Numpy 2.0dev0 wheels for macOS are lagging behind these days so I can't reproduce this easily on my development env yet. I'll revisit this soon.



Discussion / hints:
I'm also doing the work to fix the very same problem in `astropy.units` https://github.com/astropy/astropy/issues/15690, but numpy dev is a fast moving target these days, so I'll come back here when I'm done with `astropy.units`.

Fix patch:
diff --git a/unyt/_array_functions.py b/unyt/_array_functions.py
index 01e163b8..58cf5439 100644
--- a/unyt/_array_functions.py
+++ b/unyt/_array_functions.py
@@ -957,6 +957,12 @@ def asfarray(a, dtype=np.double):
         ret_units = a.units
         return np.asfarray._implementation(np.asarray(a), dtype=dtype) * ret_units
 
+elif NUMPY_VERSION >= Version("2.0.0dev0"):
+    # functions that were added in numpy 2.0.0
+    @implements(np.linalg.outer)
+    def linalg_outer(x1, x2, /):
+        return product_helper(x1, x2, out=None, func=np.linalg.outer)
+
 
 # functions with pending deprecations
 if hasattr(np, "trapz"):


Test patch:
diff --git a/unyt/testing.py b/unyt/testing.py
index 1da77886..bfe1cec9 100644
--- a/unyt/testing.py
+++ b/unyt/testing.py
@@ -75,7 +75,10 @@ def assert_array_equal_units(x, y, **kwargs):
     from numpy.testing import assert_array_equal
 
     assert_array_equal(x, y, **kwargs)
-    assert getattr(x, "units", NULL_UNIT) == getattr(y, "units", NULL_UNIT)
+    if not (xu := getattr(x, "units", NULL_UNIT)) == (
+        yu := getattr(y, "units", NULL_UNIT)
+    ):
+        raise AssertionError(f"Arguments' units do not match (got {xu} and {yu})")
 
 
 def _process_warning(op, message, warning_class, args=(), kwargs=None):
diff --git a/unyt/tests/test_array_functions.py b/unyt/tests/test_array_functions.py
index bbb37d2a..a54482a2 100644
--- a/unyt/tests/test_array_functions.py
+++ b/unyt/tests/test_array_functions.py
@@ -4,6 +4,7 @@
 
 import numpy as np
 import pytest
+from numpy.testing import assert_allclose
 from packaging.version import Version
 
 from unyt import A, K, cm, degC, delta_degC, g, km, rad, s
@@ -153,9 +154,25 @@
 
 
 if NUMPY_VERSION >= Version("2.0.0dev0"):
+    # the followin all work out of the box (tested)
     NOOP_FUNCTIONS |= {
-        np.linalg.diagonal,  # works out of the box (tested)
-        np.linalg.trace,  # works out of the box (tested)
+        np.linalg.cross,
+        np.linalg.diagonal,
+        np.linalg.matmul,
+        np.linalg.matrix_norm,
+        np.linalg.matrix_transpose,
+        np.linalg.svdvals,
+        np.linalg.tensordot,
+        np.linalg.trace,
+        np.linalg.vecdot,
+        np.linalg.vector_norm,
+        np.astype,
+        np.matrix_transpose,
+        np.unique_all,
+        np.unique_counts,
+        np.unique_inverse,
+        np.unique_values,
+        np.vecdot,
     }
 
 # Functions for which behaviour is intentionally left to default
@@ -489,6 +506,110 @@ def test_linalg_trace():
     assert b.units == a.units
 
 
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.outer is new in numpy 2.0"
+)
+def test_linalg_outer():
+    a = np.arange(10) * cm
+    assert_array_equal_units(np.linalg.outer(a, a), np.outer(a, a))
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.cross is new in numpy 2.0"
+)
+def test_linalg_cross():
+    a = np.arange(3) * cm
+    assert_array_equal_units(np.linalg.cross(a, a), np.cross(a, a))
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.matmul is new in numpy 2.0"
+)
+def test_linalg_matmul():
+    a = np.eye(3) * cm
+    assert_array_equal_units(np.linalg.matmul(a, a), np.matmul(a, a))
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"),
+    reason="linalg.matrix_norm is new in numpy 2.0",
+)
+def test_linalg_matrix_norm():
+    a = np.eye(3) * cm
+    assert_array_equal_units(np.linalg.matrix_norm(a), np.linalg.norm(a))
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="matrix_transpose is new in numpy 2.0"
+)
+@pytest.mark.parametrize("namespace", [None, "linalg"])
+def test_matrix_transpose(namespace):
+    if namespace is None:
+        func = np.matrix_transpose
+    else:
+        func = getattr(np, namespace).matrix_transpose
+    a = np.arange(0, 9).reshape(3, 3)
+    assert_array_equal_units(func(a), np.transpose(a))
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="vecdot is new in numpy 2.0"
+)
+@pytest.mark.parametrize("namespace", [None, "linalg"])
+def test_vecdot(namespace):
+    if namespace is None:
+        func = np.vecdot
+    else:
+        func = getattr(np, namespace).vecdot
+    a = np.arange(0, 9)
+    assert_array_equal_units(func(a, a), np.vdot(a, a))
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"),
+    reason="linalg.vector_norm is new in numpy 2.0",
+)
+def test_linalg_vector_norm():
+    a = np.arange(0, 9)
+    assert_array_equal_units(np.linalg.vector_norm(a), np.linalg.norm(a))
+
+
+def test_linalg_svd():
+    rng = np.random.default_rng()
+    a = (rng.standard_normal(size=(9, 6)) + 1j * rng.standard_normal(size=(9, 6))) * cm
+    u, s, vh = np.linalg.svd(a)
+    assert type(u) is np.ndarray
+    assert type(vh) is np.ndarray
+    assert type(s) is unyt_array
+    assert s.units == cm
+
+    s = np.linalg.svd(a, compute_uv=False)
+    assert type(s) is unyt_array
+    assert s.units == cm
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.svdvals is new in numpy 2.0"
+)
+def test_linalg_svdvals():
+    q = np.arange(9).reshape(3, 3) * cm
+
+    _, ref, _ = np.linalg.svd(q)
+    res = np.linalg.svdvals(q)
+    assert type(res) is unyt_array
+    assert_allclose(res, ref, rtol=5e-16)
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.tensordot is new in numpy 2.0"
+)
+def test_linalg_tensordot():
+    q = np.arange(9).reshape(3, 3) * cm
+    ref = np.tensordot(q, q)
+    res = np.linalg.tensordot(q, q)
+    assert_array_equal_units(res, ref)
+
+
 def test_histogram():
     rng = np.random.default_rng()
     arr = rng.normal(size=1000) * cm
@@ -1000,6 +1121,16 @@ def test_copyto_edge_cases():
     assert type(y) is np.ndarray
 
 
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="astype is new in numpy 2.0"
+)
+def test_astype():
+    x = np.array([1, 2, 3], dtype="int64") * cm
+    res = np.astype(x, "int32")
+    assert type(res) is unyt_array
+    assert res.units == cm
+
+
 def test_meshgrid():
     x = [1, 2, 3] * cm
     y = [1, 2, 3] * s
@@ -1261,20 +1392,6 @@ def test_eigvals(func):
     assert w.units == cm
 
 
-def test_linalg_svd():
-    rng = np.random.default_rng()
-    a = (rng.standard_normal(size=(9, 6)) + 1j * rng.standard_normal(size=(9, 6))) * cm
-    u, s, vh = np.linalg.svd(a)
-    assert type(u) is np.ndarray
-    assert type(vh) is np.ndarray
-    assert type(s) is unyt_array
-    assert s.units == cm
-
-    s = np.linalg.svd(a, compute_uv=False)
-    assert type(s) is unyt_array
-    assert s.units == cm
-
-
 def test_savetxt(tmp_path):
     a = [1, 2, 3] * cm
     with pytest.raises(
@@ -1412,6 +1529,68 @@ def test_unique():
     assert res.units == cm
 
 
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="unique_all is new in numpy 2.0"
+)
+def test_unique_all():
+    q = np.arange(9).reshape(3, 3) * cm
+    values, indices, inverse_indices, counts = np.unique(
+        q,
+        return_index=True,
+        return_inverse=True,
+        return_counts=True,
+        equal_nan=False,
+    )
+    res = np.unique_all(q)
+    assert len(res) == 4
+    assert_array_equal_units(res.values, values)
+    assert_array_equal_units(res.indices, indices)
+    assert_array_equal_units(res.inverse_indices, inverse_indices)
+    assert_array_equal_units(res.counts, counts)
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="unique_counts is new in numpy 2.0"
+)
+def test_unique_counts():
+    q = np.arange(9).reshape(3, 3) * cm
+    values, counts = np.unique(
+        q,
+        return_counts=True,
+        equal_nan=False,
+    )
+    res = np.unique_counts(q)
+    assert len(res) == 2
+    assert_array_equal_units(res.values, values)
+    assert_array_equal_units(res.counts, counts)
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="unique_inverse is new in numpy 2.0"
+)
+def test_unique_inverse():
+    q = np.arange(9).reshape(3, 3) * cm
+    values, inverse_indices = np.unique(
+        q,
+        return_inverse=True,
+        equal_nan=False,
+    )
+    res = np.unique_inverse(q)
+    assert len(res) == 2
+    assert_array_equal_units(res.values, values)
+    assert_array_equal_units(res.inverse_indices, inverse_indices)
+
+
+@pytest.mark.skipif(
+    NUMPY_VERSION < Version("2.0.0dev0"), reason="unique_values is new in numpy 2.0"
+)
+def test_unique_values():
+    q = np.arange(9).reshape(3, 3) * cm
+    values = np.unique(q, equal_nan=False)
+    res = np.unique_values(q)
+    assert_array_equal_units(res, values)
+
+
 def test_take():
     a = [1, 2, 3] * cm
     res = np.take(a, [0, 1])