From 6709082ac604991980d0817bc69bfd9ee37ff43c Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Tue, 16 Jun 2026 18:35:09 +0500 Subject: [PATCH 1/7] ENH: add nanmin --- src/array_api_extra/__init__.py | 2 ++ src/array_api_extra/_delegation.py | 42 ++++++++++++++++++++++ src/array_api_extra/_lib/_funcs.py | 21 +++++++++++ tests/test_funcs.py | 56 ++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) diff --git a/src/array_api_extra/__init__.py b/src/array_api_extra/__init__.py index 23b88043..31e0af2a 100644 --- a/src/array_api_extra/__init__.py +++ b/src/array_api_extra/__init__.py @@ -12,6 +12,7 @@ isin, kron, nan_to_num, + nanmin, one_hot, pad, partition, @@ -49,6 +50,7 @@ "kron", "lazy_apply", "nan_to_num", + "nanmin", "nunique", "one_hot", "pad", diff --git a/src/array_api_extra/_delegation.py b/src/array_api_extra/_delegation.py index 6a91bdb2..cdd516e4 100644 --- a/src/array_api_extra/_delegation.py +++ b/src/array_api_extra/_delegation.py @@ -27,6 +27,7 @@ "isclose", "kron", "nan_to_num", + "nanmin", "one_hot", "pad", "searchsorted", @@ -1318,3 +1319,44 @@ def union1d(a: Array, b: Array, /, *, xp: ModuleType | None = None) -> Array: return xp.union1d(a, b) return _funcs.union1d(a, b, xp=xp) + + +def nanmin( + a: Array, + /, + *, + axis: int | tuple[int, ...] | None = None, + xp: ModuleType | None = None, +) -> Array: + """ + Return the minimum of the array elements along a given axis, ignoring NaNs. + + Parameters + ---------- + a : Array + Input array. + + axis : int or tuple of ints or None, optional + Axis or axes along which the minimum is computed. The default is to compute + the minimum of the flattened array. + + xp : array_namespace, optional + The standard-compatible namespace for `x`. Default: infer. + + Returns + ------- + array + An array of minimum values along the given axis, ignoring NaNs. + """ + if xp is None: + xp = array_namespace(a) + + if ( + is_numpy_namespace(xp) + or is_cupy_namespace(xp) + or is_dask_namespace(xp) + or is_jax_namespace(xp) + ): + return xp.nanmin(a, axis=axis) + + return _funcs.nanmin(a, axis=axis, xp=xp) diff --git a/src/array_api_extra/_lib/_funcs.py b/src/array_api_extra/_lib/_funcs.py index 4e3b8753..59088a01 100644 --- a/src/array_api_extra/_lib/_funcs.py +++ b/src/array_api_extra/_lib/_funcs.py @@ -757,3 +757,24 @@ def angle(z: Array, /, *, deg: bool = False, xp: ModuleType | None = None) -> Ar if deg: a = a * 180 / xp.pi return a + + +def nanmin( # numpydoc ignore=PR01,RT01 + a: Array, + /, + *, + axis: int | tuple[int, ...] | None, + xp: ModuleType, +) -> Array: + """See docstring in `array_api_extra._delegation.py`.""" + mask = xp.isnan(a) + device_a = _compat.device(a) + x = xp.min( + xp.where(mask, xp.asarray(+xp.inf, dtype=a.dtype, device=device_a), a), + axis=axis, + ) + # Replace Infs from all NaN slices with NaN again + mask = xp.all(mask, axis=axis) + if xp.any(mask): + x = xp.where(mask, xp.asarray(xp.nan, dtype=x.dtype, device=device_a), x) + return x diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 2b9b01d9..74902ad6 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -27,6 +27,7 @@ isin, kron, nan_to_num, + nanmin, nunique, one_hot, pad, @@ -1983,3 +1984,58 @@ def test_2d(self, xp: ModuleType): def test_device(self, xp: ModuleType, device: Device): a = xp.asarray([1 + 1j], device=device) assert get_device(angle(a)) == device + + +class TestNanMin: + def test_simple(self, xp: ModuleType): + a = xp.asarray([[1, 2], [3, xp.nan]]) + + # with the default `axis=None` a single scalar is returned + res = nanmin(a) + expected = 1.0 + assert res == expected + + res = nanmin(a, axis=0) + expected = xp.asarray([1.0, 2.0]) + assert_equal(res, expected) + + res = nanmin(a, axis=1) + expected = xp.asarray([1.0, 3.0]) + assert_equal(res, expected) + + def test_bigger(self, xp: ModuleType): + a = xp.asarray( + [ + [1, xp.nan, 4, 5], + [xp.nan, -2, xp.nan, -4], + [2, 1, 3, xp.nan], + ] + ) + + res = nanmin(a, axis=0) + expected = xp.asarray([1.0, -2.0, 3.0, -4.0]) + assert_equal(res, expected) + + res = nanmin(a, axis=1) + expected = xp.asarray([1.0, -4.0, 1.0]) + assert_equal(res, expected) + + def test_with_infinity(self, xp: ModuleType): + a = xp.asarray([0.1, 1.0, xp.nan, xp.inf]) + res = nanmin(a) + expected = 0.1 + assert res == expected + + a = xp.asarray([0.1, 1.0, xp.nan, -xp.inf]) + res = nanmin(a) + expected = -xp.inf + assert res == expected + + @pytest.mark.skip_xp_backend( + Backend.TORCH, reason="Tensor.item() cannot be called on meta tensors" + ) + @pytest.mark.parametrize("axis", [None, 0, 1]) + def test_device(self, axis: int | None, xp: ModuleType, device: Device): + a = xp.asarray([[4, xp.nan, 1], [2, 5, xp.nan]], device=device) + res = nanmin(a, axis=axis) + assert get_device(res) == device From 214a7a3961dd14c65292dc14c0ac42405de1f8b2 Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Tue, 16 Jun 2026 18:37:16 +0500 Subject: [PATCH 2/7] Add to doc --- docs/api-assorted.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api-assorted.md b/docs/api-assorted.md index b849ab32..b764af86 100644 --- a/docs/api-assorted.md +++ b/docs/api-assorted.md @@ -20,6 +20,7 @@ isin kron nan_to_num + nanmin nunique one_hot pad From 9bd01fc015696d7c09eef03c908571b19babbc04 Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Tue, 16 Jun 2026 18:54:11 +0500 Subject: [PATCH 3/7] Add a test case where one column is all NaN --- tests/test_funcs.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 74902ad6..c25bbee6 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -2031,6 +2031,19 @@ def test_with_infinity(self, xp: ModuleType): expected = -xp.inf assert res == expected + @pytest.mark.filterwarnings("ignore::RuntimeWarning") + def test_all_nan_slice_2d(self, xp: ModuleType): + a = xp.asarray( + [ + [xp.nan, 5.0], + [xp.nan, 2.0], + ] + ) + + res = nanmin(a, axis=0, xp=xp) + expected = xp.asarray([xp.nan, 2.0]) + assert_equal(res, expected) + @pytest.mark.skip_xp_backend( Backend.TORCH, reason="Tensor.item() cannot be called on meta tensors" ) From 236f11de5556a5428e7d10d566fd2d568c0a78b8 Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Wed, 8 Jul 2026 11:39:09 +0500 Subject: [PATCH 4/7] Updates according to PR suggestions --- src/array_api_extra/_delegation.py | 16 +++++++++++++--- tests/test_funcs.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/array_api_extra/_delegation.py b/src/array_api_extra/_delegation.py index bfb65a37..0edcc7cd 100644 --- a/src/array_api_extra/_delegation.py +++ b/src/array_api_extra/_delegation.py @@ -1600,18 +1600,28 @@ def nanmin( ---------- a : Array Input array. - axis : int or tuple of ints or None, optional Axis or axes along which the minimum is computed. The default is to compute the minimum of the flattened array. - xp : array_namespace, optional - The standard-compatible namespace for `x`. Default: infer. + The standard-compatible namespace for `a`. Default: infer. Returns ------- array An array of minimum values along the given axis, ignoring NaNs. + + Examples + -------- + >>> import array_api_extra as xpx + >>> import array_api_strict as xp + >>> a = xp.asarray([[5, 3, xp.nan, 1], [4, xp.nan, 2, xp.nan]]) + >>> xpx.nanmin(a) + Array(1., dtype=array_api_strict.float64) + >>> xpx.nanmin(a, axis=0) + Array([4., 3., 2., 1.], dtype=array_api_strict.float64) + >>> xpx.nanmin(a, axis=1) + Array([1., 2.], dtype=array_api_strict.float64) """ if xp is None: xp = array_namespace(a) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 0d3a303a..4b46fb8b 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -2251,7 +2251,7 @@ def test_with_infinity(self, xp: ModuleType): expected = -xp.inf assert res == expected - @pytest.mark.filterwarnings("ignore::RuntimeWarning") + @pytest.mark.filterwarnings("ignore:.*All-NaN slice*.:RuntimeWarning") def test_all_nan_slice_2d(self, xp: ModuleType): a = xp.asarray( [ From d98cfa582228f79f68bed9dcc4194fd9480b3ed8 Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Wed, 8 Jul 2026 11:57:40 +0500 Subject: [PATCH 5/7] Add test for xp param --- src/array_api_extra/_lib/_funcs.py | 1 + tests/test_funcs.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/array_api_extra/_lib/_funcs.py b/src/array_api_extra/_lib/_funcs.py index 21be311c..7f28cbd2 100644 --- a/src/array_api_extra/_lib/_funcs.py +++ b/src/array_api_extra/_lib/_funcs.py @@ -38,6 +38,7 @@ "isin", "kron", "nan_to_num", + "nanmin", "nunique", "one_hot", "pad", diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 4b46fb8b..acbb973d 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -2272,3 +2272,12 @@ def test_device(self, axis: int | None, xp: ModuleType, device: Device): a = xp.asarray([[4, xp.nan, 1], [2, 5, xp.nan]], device=device) res = nanmin(a, axis=axis) assert get_device(res) == device + + @pytest.mark.parametrize( + ("axis", "expected_list"), [(0, [2.0, 3.0, 1.0]), (1, [1.0, 2.0])] + ) + def test_xp(self, axis: int | None, expected_list: list[float], xp: ModuleType): + a = xp.asarray([[4, xp.nan, 1], [2, 3, xp.nan]]) + res = nanmin(a, axis=axis, xp=xp) + expected: Array = xp.asarray(expected_list) + assert_equal(res, expected) From 4f261f7d9d239af54db6076331b34dd96b775e85 Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Wed, 8 Jul 2026 11:57:50 +0500 Subject: [PATCH 6/7] Add test for xp param --- tests/test_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index acbb973d..ce0894ea 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -2279,5 +2279,5 @@ def test_device(self, axis: int | None, xp: ModuleType, device: Device): def test_xp(self, axis: int | None, expected_list: list[float], xp: ModuleType): a = xp.asarray([[4, xp.nan, 1], [2, 3, xp.nan]]) res = nanmin(a, axis=axis, xp=xp) - expected: Array = xp.asarray(expected_list) + expected = xp.asarray(expected_list) assert_equal(res, expected) From 7822f6cf0a50dbfb6e21200ba22a91770859c3ce Mon Sep 17 00:00:00 2001 From: Omar Salman Date: Wed, 8 Jul 2026 12:06:56 +0500 Subject: [PATCH 7/7] Add test for a simple scalar --- tests/test_funcs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index ce0894ea..4e6d69f0 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -2251,6 +2251,10 @@ def test_with_infinity(self, xp: ModuleType): expected = -xp.inf assert res == expected + def test_scalar(self, xp: ModuleType): + a = xp.asarray(1.0) + assert nanmin(a) == 1.0 + @pytest.mark.filterwarnings("ignore:.*All-NaN slice*.:RuntimeWarning") def test_all_nan_slice_2d(self, xp: ModuleType): a = xp.asarray(