From 660e287f8e4ded698d4d4f9d60c15557451903e0 Mon Sep 17 00:00:00 2001 From: carlosgmartin Date: Mon, 17 Feb 2025 14:48:51 -0500 Subject: [PATCH 1/6] BUG: Fix linalg.norm to handle empty matrices correctly. --- numpy/linalg/_linalg.py | 22 +++++++++++----------- numpy/linalg/tests/test_linalg.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/numpy/linalg/_linalg.py b/numpy/linalg/_linalg.py index 32a52b7b8cdb..f9809839d80e 100644 --- a/numpy/linalg/_linalg.py +++ b/numpy/linalg/_linalg.py @@ -2541,7 +2541,7 @@ def lstsq(a, b, rcond=None): return wrap(x), wrap(resids), rank, s -def _multi_svd_norm(x, row_axis, col_axis, op): +def _multi_svd_norm(x, row_axis, col_axis, op, initial): """Compute a function of the singular values of the 2-D matrices in `x`. This is a private utility function used by `numpy.linalg.norm()`. @@ -2565,7 +2565,7 @@ def _multi_svd_norm(x, row_axis, col_axis, op): """ y = moveaxis(x, (row_axis, col_axis), (-2, -1)) - result = op(svd(y, compute_uv=False), axis=-1) + result = op(svd(y, compute_uv=False), axis=-1, initial=initial) return result @@ -2763,9 +2763,9 @@ def norm(x, ord=None, axis=None, keepdims=False): if len(axis) == 1: if ord == inf: - return abs(x).max(axis=axis, keepdims=keepdims) + return abs(x).max(axis=axis, keepdims=keepdims, initial=0) elif ord == -inf: - return abs(x).min(axis=axis, keepdims=keepdims) + return abs(x).min(axis=axis, keepdims=keepdims, initial=inf) elif ord == 0: # Zero norm return ( @@ -2797,29 +2797,29 @@ def norm(x, ord=None, axis=None, keepdims=False): if row_axis == col_axis: raise ValueError('Duplicate axes given.') if ord == 2: - ret = _multi_svd_norm(x, row_axis, col_axis, amax) + ret = _multi_svd_norm(x, row_axis, col_axis, amax, 0) elif ord == -2: - ret = _multi_svd_norm(x, row_axis, col_axis, amin) + ret = _multi_svd_norm(x, row_axis, col_axis, amin, inf) elif ord == 1: if col_axis > row_axis: col_axis -= 1 - ret = add.reduce(abs(x), axis=row_axis).max(axis=col_axis) + ret = add.reduce(abs(x), axis=row_axis).max(axis=col_axis, initial=0) elif ord == inf: if row_axis > col_axis: row_axis -= 1 - ret = add.reduce(abs(x), axis=col_axis).max(axis=row_axis) + ret = add.reduce(abs(x), axis=col_axis).max(axis=row_axis, initial=0) elif ord == -1: if col_axis > row_axis: col_axis -= 1 - ret = add.reduce(abs(x), axis=row_axis).min(axis=col_axis) + ret = add.reduce(abs(x), axis=row_axis).min(axis=col_axis, initial=inf) elif ord == -inf: if row_axis > col_axis: row_axis -= 1 - ret = add.reduce(abs(x), axis=col_axis).min(axis=row_axis) + ret = add.reduce(abs(x), axis=col_axis).min(axis=row_axis, initial=inf) elif ord in [None, 'fro', 'f']: ret = sqrt(add.reduce((x.conj() * x).real, axis=axis)) elif ord == 'nuc': - ret = _multi_svd_norm(x, row_axis, col_axis, sum) + ret = _multi_svd_norm(x, row_axis, col_axis, sum, 0) else: raise ValueError("Invalid norm order for matrices.") if keepdims: diff --git a/numpy/linalg/tests/test_linalg.py b/numpy/linalg/tests/test_linalg.py index 356676e7ce6d..dd1e7bd47c64 100644 --- a/numpy/linalg/tests/test_linalg.py +++ b/numpy/linalg/tests/test_linalg.py @@ -2372,6 +2372,26 @@ def test_matrix_norm(): assert_almost_equal(actual, np.array([[14.2828]]), double_decimal=3) +def test_matrix_norm_empty(): + for shape in [(0, 2), (2, 0), (0, 0)]: + for dtype in [np.float64, np.float32, np.int32]: + x = np.zeros(shape, dtype) + + assert_equal(np.linalg.matrix_norm(x, ord="fro"), 0) + assert_equal(np.linalg.matrix_norm(x, ord="nuc"), 0) + + assert_equal(np.linalg.matrix_norm(x, ord=2), 0) + assert_equal(np.linalg.matrix_norm(x, ord=-2), np.inf) + + assert_equal(np.linalg.matrix_norm(x, ord=1), 0) + y = np.inf if x.shape[1] == 0 else 0 + assert_equal(np.linalg.matrix_norm(x, ord=-1), y) + + assert_equal(np.linalg.matrix_norm(x, ord=np.inf), 0) + z = np.inf if x.shape[0] == 0 else 0 + assert_equal(np.linalg.matrix_norm(x, ord=-np.inf), z) + + def test_vector_norm(): x = np.arange(9).reshape((3, 3)) actual = np.linalg.vector_norm(x) @@ -2388,3 +2408,13 @@ def test_vector_norm(): expected = np.full((1, 1), 14.2828, dtype='float64') assert_equal(actual.shape, expected.shape) assert_almost_equal(actual, expected, double_decimal=3) + + +def test_vector_norm_empty(): + for dtype in [np.float64, np.float32, np.int32]: + x = np.zeros(0, dtype) + assert_equal(np.linalg.vector_norm(x, ord=0), 0) + assert_equal(np.linalg.vector_norm(x, ord=1), 0) + assert_equal(np.linalg.vector_norm(x, ord=2), 0) + assert_equal(np.linalg.vector_norm(x, ord=np.inf), 0) + assert_equal(np.linalg.vector_norm(x, ord=-np.inf), np.inf) From cf755ce806c19eba28758fbb32756adcc52d289e Mon Sep 17 00:00:00 2001 From: carlosgmartin Date: Fri, 21 Feb 2025 17:17:32 -0500 Subject: [PATCH 2/6] Restrict changes to proper norms. --- numpy/linalg/_linalg.py | 10 +++++----- numpy/linalg/tests/test_linalg.py | 14 +------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/numpy/linalg/_linalg.py b/numpy/linalg/_linalg.py index f9809839d80e..f5bc524a27ae 100644 --- a/numpy/linalg/_linalg.py +++ b/numpy/linalg/_linalg.py @@ -2541,7 +2541,7 @@ def lstsq(a, b, rcond=None): return wrap(x), wrap(resids), rank, s -def _multi_svd_norm(x, row_axis, col_axis, op, initial): +def _multi_svd_norm(x, row_axis, col_axis, op, initial=None): """Compute a function of the singular values of the 2-D matrices in `x`. This is a private utility function used by `numpy.linalg.norm()`. @@ -2765,7 +2765,7 @@ def norm(x, ord=None, axis=None, keepdims=False): if ord == inf: return abs(x).max(axis=axis, keepdims=keepdims, initial=0) elif ord == -inf: - return abs(x).min(axis=axis, keepdims=keepdims, initial=inf) + return abs(x).min(axis=axis, keepdims=keepdims) elif ord == 0: # Zero norm return ( @@ -2799,7 +2799,7 @@ def norm(x, ord=None, axis=None, keepdims=False): if ord == 2: ret = _multi_svd_norm(x, row_axis, col_axis, amax, 0) elif ord == -2: - ret = _multi_svd_norm(x, row_axis, col_axis, amin, inf) + ret = _multi_svd_norm(x, row_axis, col_axis, amin) elif ord == 1: if col_axis > row_axis: col_axis -= 1 @@ -2811,11 +2811,11 @@ def norm(x, ord=None, axis=None, keepdims=False): elif ord == -1: if col_axis > row_axis: col_axis -= 1 - ret = add.reduce(abs(x), axis=row_axis).min(axis=col_axis, initial=inf) + ret = add.reduce(abs(x), axis=row_axis).min(axis=col_axis) elif ord == -inf: if row_axis > col_axis: row_axis -= 1 - ret = add.reduce(abs(x), axis=col_axis).min(axis=row_axis, initial=inf) + ret = add.reduce(abs(x), axis=col_axis).min(axis=row_axis) elif ord in [None, 'fro', 'f']: ret = sqrt(add.reduce((x.conj() * x).real, axis=axis)) elif ord == 'nuc': diff --git a/numpy/linalg/tests/test_linalg.py b/numpy/linalg/tests/test_linalg.py index dd1e7bd47c64..1a79629814e9 100644 --- a/numpy/linalg/tests/test_linalg.py +++ b/numpy/linalg/tests/test_linalg.py @@ -2376,21 +2376,11 @@ def test_matrix_norm_empty(): for shape in [(0, 2), (2, 0), (0, 0)]: for dtype in [np.float64, np.float32, np.int32]: x = np.zeros(shape, dtype) - assert_equal(np.linalg.matrix_norm(x, ord="fro"), 0) assert_equal(np.linalg.matrix_norm(x, ord="nuc"), 0) - - assert_equal(np.linalg.matrix_norm(x, ord=2), 0) - assert_equal(np.linalg.matrix_norm(x, ord=-2), np.inf) - assert_equal(np.linalg.matrix_norm(x, ord=1), 0) - y = np.inf if x.shape[1] == 0 else 0 - assert_equal(np.linalg.matrix_norm(x, ord=-1), y) - + assert_equal(np.linalg.matrix_norm(x, ord=2), 0) assert_equal(np.linalg.matrix_norm(x, ord=np.inf), 0) - z = np.inf if x.shape[0] == 0 else 0 - assert_equal(np.linalg.matrix_norm(x, ord=-np.inf), z) - def test_vector_norm(): x = np.arange(9).reshape((3, 3)) @@ -2413,8 +2403,6 @@ def test_vector_norm(): def test_vector_norm_empty(): for dtype in [np.float64, np.float32, np.int32]: x = np.zeros(0, dtype) - assert_equal(np.linalg.vector_norm(x, ord=0), 0) assert_equal(np.linalg.vector_norm(x, ord=1), 0) assert_equal(np.linalg.vector_norm(x, ord=2), 0) assert_equal(np.linalg.vector_norm(x, ord=np.inf), 0) - assert_equal(np.linalg.vector_norm(x, ord=-np.inf), np.inf) From 30890e7f766c7d542b21e63887cf655770fdd79e Mon Sep 17 00:00:00 2001 From: carlosgmartin Date: Sat, 1 Mar 2025 15:09:32 -0500 Subject: [PATCH 3/6] Add change release note. --- doc/release/upcoming_changes/28343.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release/upcoming_changes/28343.change.rst diff --git a/doc/release/upcoming_changes/28343.change.rst b/doc/release/upcoming_changes/28343.change.rst new file mode 100644 index 000000000000..3c1f89500669 --- /dev/null +++ b/doc/release/upcoming_changes/28343.change.rst @@ -0,0 +1 @@ +* ``np.linalg.norm`` now yields zero for proper norms of empty vectors and matrices. From a6e6aeaa5a7408958927064599f6ef11b508f207 Mon Sep 17 00:00:00 2001 From: carlosgmartin Date: Wed, 5 Mar 2025 17:21:54 -0500 Subject: [PATCH 4/6] Edit release note. --- doc/release/upcoming_changes/28343.change.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/upcoming_changes/28343.change.rst b/doc/release/upcoming_changes/28343.change.rst index 3c1f89500669..0a74a1613837 100644 --- a/doc/release/upcoming_changes/28343.change.rst +++ b/doc/release/upcoming_changes/28343.change.rst @@ -1 +1 @@ -* ``np.linalg.norm`` now yields zero for proper norms of empty vectors and matrices. +* Currently, ``np.linalg.norm``, ``np.linalg.vector_norm``, and ``np.linalg.matrix_norm`` raise errors for some zero-sized inputs. This change ensures that any proper norm of a zero-sized input is zero, which makes it consistent with the mathematical properties of a norm. From a99d6727f6401407cc0a1e9a20bba6e701552fb5 Mon Sep 17 00:00:00 2001 From: Carlos Martin Date: Thu, 6 Mar 2025 12:08:31 -0500 Subject: [PATCH 5/6] Update doc/release/upcoming_changes/28343.change.rst Co-authored-by: Nathan Goldbaum --- doc/release/upcoming_changes/28343.change.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/upcoming_changes/28343.change.rst b/doc/release/upcoming_changes/28343.change.rst index 0a74a1613837..d25ef10eeb46 100644 --- a/doc/release/upcoming_changes/28343.change.rst +++ b/doc/release/upcoming_changes/28343.change.rst @@ -1 +1 @@ -* Currently, ``np.linalg.norm``, ``np.linalg.vector_norm``, and ``np.linalg.matrix_norm`` raise errors for some zero-sized inputs. This change ensures that any proper norm of a zero-sized input is zero, which makes it consistent with the mathematical properties of a norm. +* In all cases, ``np.linalg.norm``, ``np.linalg.vector_norm``, and ``np.linalg.matrix_norm`` now return zero for arrays with a shape of zero along at least one axis. Previously, NumPy would raises errors or return zero depending on the shape of the array. From 9bcf80c530653e97578935884d8bd4bd659b1086 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 7 Mar 2025 09:29:06 -0700 Subject: [PATCH 6/6] Update doc/release/upcoming_changes/28343.change.rst --- doc/release/upcoming_changes/28343.change.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/upcoming_changes/28343.change.rst b/doc/release/upcoming_changes/28343.change.rst index d25ef10eeb46..378ef775b62e 100644 --- a/doc/release/upcoming_changes/28343.change.rst +++ b/doc/release/upcoming_changes/28343.change.rst @@ -1 +1 @@ -* In all cases, ``np.linalg.norm``, ``np.linalg.vector_norm``, and ``np.linalg.matrix_norm`` now return zero for arrays with a shape of zero along at least one axis. Previously, NumPy would raises errors or return zero depending on the shape of the array. +* The vector norm ``ord=inf`` and the matrix norms ``ord={1, 2, inf, 'nuc'}`` now always returns zero for empty arrays. Empty arrays have at least one axis of size zero. This affects `np.linalg.norm`, `np.linalg.vector_norm`, and `np.linalg.matrix_norm`. Previously, NumPy would raises errors or return zero depending on the shape of the array.