From 520bbea2dde07c1caedfba8ed2cb3bd343a5a7f1 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 23 Nov 2022 14:18:58 +0100 Subject: [PATCH 01/23] implement nep35 --- pint/facets/numpy/numpy_func.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 2c07281b8..86291d786 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -934,6 +934,30 @@ def implementation(a, *args, **kwargs): implement_func("function", func_str, input_units=None, output_unit="variance") +def implement_nep35_func(func_str): + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + + func = getattr(np, func_str) + + @implements(func_str, "function") + def implementation(*args, like, **kwargs): + result = func(*args, **kwargs) + return like._REGISTRY.Quantity(result, like.units) + + +nep35_function_names = { + "array", + "asarray", + "arange", + "linspace", + "identity", +} +for func_str in nep35_function_names: + implement_nep35_func(func_str) + + def numpy_wrap(func_type, func, args, kwargs, types): """Return the result from a NumPy function/ufunc as wrapped by Pint.""" From 9d4b4cb228ff81484ccbb61654da38170e901b00 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 23 Nov 2022 14:19:12 +0100 Subject: [PATCH 02/23] hook nep 35 into Quantity --- pint/facets/numpy/quantity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pint/facets/numpy/quantity.py b/pint/facets/numpy/quantity.py index 243610033..5ef1042e6 100644 --- a/pint/facets/numpy/quantity.py +++ b/pint/facets/numpy/quantity.py @@ -22,6 +22,7 @@ get_op_output_unit, matching_input_copy_units_output_ufuncs, matching_input_set_units_output_ufuncs, + nep35_function_names, numpy_wrap, op_units_output_ufuncs, set_units_ufuncs, @@ -61,6 +62,10 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): return numpy_wrap("ufunc", ufunc, inputs, kwargs, types) def __array_function__(self, func, types, args, kwargs): + nep35_functions = {getattr(np, name) for name in nep35_function_names} + if func in nep35_functions: + kwargs["like"] = self + return numpy_wrap("function", func, args, kwargs, types) _wrapped_numpy_methods = ["flatten", "astype", "item"] From 2776a0a9506fcc106f177f585a82d9a08919b33c Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 23 Nov 2022 14:39:19 +0100 Subject: [PATCH 03/23] more nep 35 functions --- pint/facets/numpy/numpy_func.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 86291d786..bc8be5b95 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -950,9 +950,11 @@ def implementation(*args, like, **kwargs): nep35_function_names = { "array", "asarray", + "asanyarray", "arange", "linspace", "identity", + "eye", } for func_str in nep35_function_names: implement_nep35_func(func_str) From 2b81f82e7644abe333e730ad515343e1a44da6c9 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 16:16:52 +0100 Subject: [PATCH 04/23] tests for *_like --- pint/testsuite/test_numpy.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 83448ce0f..1dd482f57 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -57,17 +57,22 @@ class TestNumpyArrayCreation(TestNumpyMethods): @helpers.requires_array_function_protocol() def test_ones_like(self): - self.assertNDArrayEqual(np.ones_like(self.q), np.array([[1, 1], [1, 1]])) + helpers.assert_quantity_equal( + np.ones_like(self.q), self.Q_([[1, 1], [1, 1]], self.q.units) + ) @helpers.requires_array_function_protocol() def test_zeros_like(self): - self.assertNDArrayEqual(np.zeros_like(self.q), np.array([[0, 0], [0, 0]])) + helpers.assert_quantity_equal( + np.zeros_like(self.q), self.Q_([[0, 0], [0, 0]], self.q.units) + ) @helpers.requires_array_function_protocol() def test_empty_like(self): ret = np.empty_like(self.q) - assert ret.shape == (2, 2) - assert isinstance(ret, np.ndarray) + expected = self.Q_(np.empty_like(self.q.magnitude), self.q.units) + + helpers.assert_quantity_equal(ret, expected) @helpers.requires_array_function_protocol() def test_full_like(self): From e5d272af33cb6b85da8512a73a37411c24747e71 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 16:28:43 +0100 Subject: [PATCH 05/23] change the behavior of `ones_like`, `zeros_like`, and `empty_like` --- pint/facets/numpy/numpy_func.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index bc8be5b95..075849b5b 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -903,9 +903,6 @@ def implementation(a, *args, **kwargs): "isreal", "iscomplex", "shape", - "ones_like", - "zeros_like", - "empty_like", "argsort", "argmin", "argmax", @@ -934,6 +931,10 @@ def implementation(a, *args, **kwargs): implement_func("function", func_str, input_units=None, output_unit="variance") +for func_str in ["ones_like", "zeros_like", "empty_like"]: + implement_func("function", func_str, input_units=None, output_unit="match_input") + + def implement_nep35_func(func_str): # If NumPy is not available, do not attempt implement that which does not exist if np is None: From 0e97b83218077c44fa7de255761243d73618bef3 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 16:29:39 +0100 Subject: [PATCH 06/23] refactor the `full_like` implementation --- pint/facets/numpy/numpy_func.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 075849b5b..a5dfd0c9c 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -530,18 +530,19 @@ def _full_like(a, fill_value, dtype=None, order="K", subok=True, shape=None): # Make full_like by multiplying with array from ones_like in a # non-multiplicative-unit-safe way if hasattr(fill_value, "_REGISTRY"): - return fill_value._REGISTRY.Quantity( - ( - np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) - * fill_value.m - ), - fill_value.units, - ) + units = fill_value.units + fill_value_ = fill_value.m else: - return ( - np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) - * fill_value - ) + units = None + fill_value_ = fill_value + + magnitude = np.full_like( + a.m, dtype=dtype, order=order, subok=subok, shape=shape, fill_value=fill_value_ + ) + if units is not None: + return fill_value._REGISTRY.Quantity(magnitude, units) + else: + return magnitude @implements("interp", "function") From f4f908a65016a99253081e28fd438569e5cbe1fd Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 17:02:56 +0100 Subject: [PATCH 07/23] tests for the nep35 functions --- pint/facets/numpy/numpy_func.py | 1 - pint/testsuite/test_numpy.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index a5dfd0c9c..17b839096 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -954,7 +954,6 @@ def implementation(*args, like, **kwargs): "asarray", "asanyarray", "arange", - "linspace", "identity", "eye", } diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 1dd482f57..bb4ff5901 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -82,6 +82,47 @@ def test_full_like(self): ) self.assertNDArrayEqual(np.full_like(self.q, 2), np.array([[2, 2], [2, 2]])) + def test_array(self): + x = [0, 1, 2, 3] + actual = np.array(x, like=self.q) + expected = self.Q_(x, self.q.units) + + helpers.assert_quantity_equal(actual, expected) + + def test_asarray(self): + x = [0, 1, 2, 3] + actual = np.asarray(x, like=self.q) + expected = self.Q_(x, self.q.units) + + helpers.assert_quantity_equal(actual, expected) + + def test_asanyarray(self): + x = [0, 1, 2, 3] + actual = np.asanyarray(x, like=self.q) + expected = self.Q_(x, self.q.units) + + helpers.assert_quantity_equal(actual, expected) + + def test_arange(self): + actual = np.arange(10, like=self.q) + expected = self.Q_(np.arange(10), self.q.units) + + helpers.assert_quantity_equal(actual, expected) + + @helpers.requires_numpy_at_least("1.23.0") + def test_identity(self): + actual = np.identity(10, like=self.q) + expected = self.Q_(np.identity(10), self.q.units) + + helpers.assert_quantity_equal(actual, expected) + + @helpers.requires_numpy_at_least("1.23.0") + def test_eye(self): + actual = np.eye(10, like=self.q) + expected = self.Q_(np.eye(10), self.q.units) + + helpers.assert_quantity_equal(actual, expected) + class TestNumpyArrayManipulation(TestNumpyMethods): # TODO From 5220226e4bda52ced29e9c1994c187e64da7f974 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 17:03:10 +0100 Subject: [PATCH 08/23] also implement `ones`, `zeros`, and `empty` --- pint/facets/numpy/numpy_func.py | 3 +++ pint/testsuite/test_numpy.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 17b839096..357b4e832 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -954,6 +954,9 @@ def implementation(*args, like, **kwargs): "asarray", "asanyarray", "arange", + "ones", + "zeros", + "empty", "identity", "eye", } diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index bb4ff5901..f34f5658f 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -109,6 +109,28 @@ def test_arange(self): helpers.assert_quantity_equal(actual, expected) + @helpers.requires_numpy_at_least("1.23.0") + def test_ones(self): + shape = (2, 3) + actual = np.ones(shape=shape, like=self.q) + expected = self.Q_(np.ones(shape=shape), self.q.units) + + helpers.assert_quantity_equal(actual, expected) + + def test_zeros(self): + shape = (2, 3) + actual = np.zeros(shape=shape, like=self.q) + expected = self.Q_(np.zeros(shape=shape), self.q.units) + + helpers.assert_quantity_equal(actual, expected) + + def test_empty(self): + shape = (2, 3) + actual = np.empty(shape=shape, like=self.q) + expected = self.Q_(np.empty(shape=shape), self.q.units) + + helpers.assert_quantity_equal(actual, expected) + @helpers.requires_numpy_at_least("1.23.0") def test_identity(self): actual = np.identity(10, like=self.q) From 50126c35f3f0a7e44a9492b1ffe794b8b439e86d Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 17:05:27 +0100 Subject: [PATCH 09/23] add comments detailing why a minimum version is required --- pint/testsuite/test_numpy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index f34f5658f..348988916 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -109,6 +109,7 @@ def test_arange(self): helpers.assert_quantity_equal(actual, expected) + # before 1.23.0, ones seems to be a pure python function with changing address @helpers.requires_numpy_at_least("1.23.0") def test_ones(self): shape = (2, 3) @@ -131,6 +132,7 @@ def test_empty(self): helpers.assert_quantity_equal(actual, expected) + # before 1.23.0, identity seems to be a pure python function with changing address @helpers.requires_numpy_at_least("1.23.0") def test_identity(self): actual = np.identity(10, like=self.q) @@ -138,6 +140,7 @@ def test_identity(self): helpers.assert_quantity_equal(actual, expected) + # before 1.23.0, eye seems to be a pure python function with changing address @helpers.requires_numpy_at_least("1.23.0") def test_eye(self): actual = np.eye(10, like=self.q) From 9327a0faa5221ec3ec4a133de834f9f21f8e99c0 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 17:09:08 +0100 Subject: [PATCH 10/23] define the behavior and a test for `full` --- pint/testsuite/test_numpy.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 348988916..6ceec2780 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -132,6 +132,19 @@ def test_empty(self): helpers.assert_quantity_equal(actual, expected) + def test_full(self): + shape = (2, 2) + + actual = np.full( + shape=shape, fill_value=self.Q_(0, self.ureg.degC), like=self.q + ) + expected = self.Q_([[0, 0], [0, 0]], self.ureg.degC) + helpers.assert_quantity_equal(actual, expected) + + actual = np.full(shape=shape, fill_value=2, like=self.q) + expected = self.Q_([[2, 2], [2, 2]], "dimensionless") + helpers.assert_quantity_equal(actual, expected) + # before 1.23.0, identity seems to be a pure python function with changing address @helpers.requires_numpy_at_least("1.23.0") def test_identity(self): From 032c5609db9624d3eced14e5a1903192638994d3 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 17:38:33 +0100 Subject: [PATCH 11/23] implement nep35 support for `full` --- pint/facets/numpy/numpy_func.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 357b4e832..ae7b89156 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -964,6 +964,27 @@ def implementation(*args, like, **kwargs): implement_nep35_func(func_str) +@implements("full", "function") +def _full(shape, fill_value, dtype=None, order="C", *, like): + # Make full_like by multiplying with array from ones_like in a + # non-multiplicative-unit-safe way + if hasattr(fill_value, "_REGISTRY"): + units = fill_value.units + fill_value_ = fill_value.m + else: + units = None + fill_value_ = fill_value + + magnitude = np.full(shape=shape, fill_value=fill_value_, dtype=dtype, order=order) + if units is not None: + return fill_value._REGISTRY.Quantity(magnitude, units) + else: + return like._REGISTRY.Quantity(magnitude, units) + + +nep35_function_names.add("full") + + def numpy_wrap(func_type, func, args, kwargs, types): """Return the result from a NumPy function/ufunc as wrapped by Pint.""" From 19b09b1b4823b034b140712a61f436a2aa3268a4 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 17:44:07 +0100 Subject: [PATCH 12/23] pass the magnitude to `like` --- pint/facets/numpy/numpy_func.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index ae7b89156..332c84ed7 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -945,7 +945,7 @@ def implement_nep35_func(func_str): @implements(func_str, "function") def implementation(*args, like, **kwargs): - result = func(*args, **kwargs) + result = func(*args, like=like.magnitude, **kwargs) return like._REGISTRY.Quantity(result, like.units) @@ -975,7 +975,13 @@ def _full(shape, fill_value, dtype=None, order="C", *, like): units = None fill_value_ = fill_value - magnitude = np.full(shape=shape, fill_value=fill_value_, dtype=dtype, order=order) + magnitude = np.full( + shape=shape, + fill_value=fill_value_, + dtype=dtype, + order=order, + like=like.magnitude, + ) if units is not None: return fill_value._REGISTRY.Quantity(magnitude, units) else: From af31586f3ac2784d42ea684a1fec95a261ffa2f0 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 17:51:52 +0100 Subject: [PATCH 13/23] make sure the arguments have consistent units --- pint/facets/numpy/numpy_func.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 332c84ed7..96d1d725d 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -945,6 +945,7 @@ def implement_nep35_func(func_str): @implements(func_str, "function") def implementation(*args, like, **kwargs): + args, kwargs = convert_to_consistent_units(*args, **kwargs) result = func(*args, like=like.magnitude, **kwargs) return like._REGISTRY.Quantity(result, like.units) From c906a30ff331eefbd7fb2c2ef7a830dd10ebe3ea Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 18:55:22 +0100 Subject: [PATCH 14/23] require numpy>=1.20.0 for NEP35 --- pint/testsuite/helpers.py | 7 +++++++ pint/testsuite/test_numpy.py | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index d72b5a319..e6d3add6b 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -109,6 +109,13 @@ def requires_not_array_function_protocol(): ) +def requires_numpy_nep35(): + return pytest.mark.skipif( + not LooseVersion(NUMPY_VER) < LooseVersion("1.20.0"), + reason="Needs NEP 35, which is supported from numpy=1.20.0", + ) + + def requires_numpy_previous_than(version): if not HAS_NUMPY: return pytest.mark.skip("Requires NumPy") diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 6ceec2780..fe888d9d5 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -82,6 +82,7 @@ def test_full_like(self): ) self.assertNDArrayEqual(np.full_like(self.q, 2), np.array([[2, 2], [2, 2]])) + @helpers.requires_numpy_nep35() def test_array(self): x = [0, 1, 2, 3] actual = np.array(x, like=self.q) @@ -89,6 +90,7 @@ def test_array(self): helpers.assert_quantity_equal(actual, expected) + @helpers.requires_numpy_nep35() def test_asarray(self): x = [0, 1, 2, 3] actual = np.asarray(x, like=self.q) @@ -96,6 +98,7 @@ def test_asarray(self): helpers.assert_quantity_equal(actual, expected) + @helpers.requires_numpy_nep35() def test_asanyarray(self): x = [0, 1, 2, 3] actual = np.asanyarray(x, like=self.q) @@ -103,6 +106,7 @@ def test_asanyarray(self): helpers.assert_quantity_equal(actual, expected) + @helpers.requires_numpy_nep35() def test_arange(self): actual = np.arange(10, like=self.q) expected = self.Q_(np.arange(10), self.q.units) @@ -111,6 +115,7 @@ def test_arange(self): # before 1.23.0, ones seems to be a pure python function with changing address @helpers.requires_numpy_at_least("1.23.0") + @helpers.requires_numpy_nep35() def test_ones(self): shape = (2, 3) actual = np.ones(shape=shape, like=self.q) @@ -118,6 +123,7 @@ def test_ones(self): helpers.assert_quantity_equal(actual, expected) + @helpers.requires_numpy_nep35() def test_zeros(self): shape = (2, 3) actual = np.zeros(shape=shape, like=self.q) @@ -125,6 +131,7 @@ def test_zeros(self): helpers.assert_quantity_equal(actual, expected) + @helpers.requires_numpy_nep35() def test_empty(self): shape = (2, 3) actual = np.empty(shape=shape, like=self.q) @@ -132,6 +139,7 @@ def test_empty(self): helpers.assert_quantity_equal(actual, expected) + @helpers.requires_numpy_nep35() def test_full(self): shape = (2, 2) @@ -147,6 +155,7 @@ def test_full(self): # before 1.23.0, identity seems to be a pure python function with changing address @helpers.requires_numpy_at_least("1.23.0") + @helpers.requires_numpy_nep35() def test_identity(self): actual = np.identity(10, like=self.q) expected = self.Q_(np.identity(10), self.q.units) @@ -155,6 +164,7 @@ def test_identity(self): # before 1.23.0, eye seems to be a pure python function with changing address @helpers.requires_numpy_at_least("1.23.0") + @helpers.requires_numpy_nep35() def test_eye(self): actual = np.eye(10, like=self.q) expected = self.Q_(np.eye(10), self.q.units) From bdb6f36a478f1602beeb2009e3640a9cd53b0899 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 18:56:20 +0100 Subject: [PATCH 15/23] support quantities as arguments to `arange` --- pint/testsuite/test_numpy.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index fe888d9d5..54bdb95ff 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -110,7 +110,15 @@ def test_asanyarray(self): def test_arange(self): actual = np.arange(10, like=self.q) expected = self.Q_(np.arange(10), self.q.units) + helpers.assert_quantity_equal(actual, expected) + actual = np.arange( + self.Q_(1, "kg"), + self.Q_(5, "kg"), + self.Q_(100, "g"), + like=self.Q_([0], "kg"), + ) + expected = self.Q_(np.arange(1, 5, 0.1), "kg") helpers.assert_quantity_equal(actual, expected) # before 1.23.0, ones seems to be a pure python function with changing address From 9ac0e3ecd9d8ea5085944f1b2c3c3720caa5332e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 20:06:38 +0100 Subject: [PATCH 16/23] fix the skip --- pint/testsuite/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index e6d3add6b..c2cec045b 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -111,7 +111,7 @@ def requires_not_array_function_protocol(): def requires_numpy_nep35(): return pytest.mark.skipif( - not LooseVersion(NUMPY_VER) < LooseVersion("1.20.0"), + not LooseVersion(NUMPY_VER) >= LooseVersion("1.20.0"), reason="Needs NEP 35, which is supported from numpy=1.20.0", ) From 555b79639bed5f92c737e539faf859eafc62f5ef Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 20:27:11 +0100 Subject: [PATCH 17/23] implement arange to support quantities for `start`, `stop`, `step` --- pint/facets/numpy/numpy_func.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 96d1d725d..2fe918570 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -964,6 +964,8 @@ def implementation(*args, like, **kwargs): for func_str in nep35_function_names: implement_nep35_func(func_str) +nep35_function_names.union({"full", "arange"}) + @implements("full", "function") def _full(shape, fill_value, dtype=None, order="C", *, like): @@ -989,7 +991,21 @@ def _full(shape, fill_value, dtype=None, order="C", *, like): return like._REGISTRY.Quantity(magnitude, units) -nep35_function_names.add("full") +@implements("arange", "function") +def _arange(start, stop=None, step=None, dtype=None, *, like): + args = [start, stop, step] + if any(_is_quantity(arg) for arg in args): + args, kwargs = convert_to_consistent_units( + start, + stop, + step, + pre_calc_units=like.units, + like=like, + ) + else: + kwargs = {"like": like.magnitude} + + return like._REGISTRY.Quantity(np.arange(*args, dtype=dtype, **kwargs), like.units) def numpy_wrap(func_type, func, args, kwargs, types): From d88d06a04597a76003c5f7ba25b9b29f464b5c24 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Nov 2022 20:35:38 +0100 Subject: [PATCH 18/23] actually append the additional functions --- pint/facets/numpy/numpy_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 2fe918570..2cea91371 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -964,7 +964,7 @@ def implementation(*args, like, **kwargs): for func_str in nep35_function_names: implement_nep35_func(func_str) -nep35_function_names.union({"full", "arange"}) +nep35_function_names.update({"full", "arange"}) @implements("full", "function") From 5cfad026a8b3b6724c9ef10b22adc873965835aa Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 19 Dec 2022 22:52:41 +0100 Subject: [PATCH 19/23] use a decorator to do the functon registration --- pint/facets/numpy/numpy_func.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 2cea91371..4cd8f1735 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -936,6 +936,18 @@ def implementation(a, *args, **kwargs): implement_func("function", func_str, input_units=None, output_unit="match_input") +nep35_function_names = set() + + +def register_nep35_function(func_str): + nep35_function_names.add(func_str) + + def wrapper(f): + return f + + return wrapper + + def implement_nep35_func(func_str): # If NumPy is not available, do not attempt implement that which does not exist if np is None: @@ -943,6 +955,7 @@ def implement_nep35_func(func_str): func = getattr(np, func_str) + @register_nep35_function(func_str) @implements(func_str, "function") def implementation(*args, like, **kwargs): args, kwargs = convert_to_consistent_units(*args, **kwargs) @@ -950,7 +963,8 @@ def implementation(*args, like, **kwargs): return like._REGISTRY.Quantity(result, like.units) -nep35_function_names = { +# generic implementations +for func_str in { "array", "asarray", "asanyarray", @@ -960,13 +974,11 @@ def implementation(*args, like, **kwargs): "empty", "identity", "eye", -} -for func_str in nep35_function_names: +}: implement_nep35_func(func_str) -nep35_function_names.update({"full", "arange"}) - +@register_nep35_function("full") @implements("full", "function") def _full(shape, fill_value, dtype=None, order="C", *, like): # Make full_like by multiplying with array from ones_like in a @@ -991,6 +1003,7 @@ def _full(shape, fill_value, dtype=None, order="C", *, like): return like._REGISTRY.Quantity(magnitude, units) +@register_nep35_function("arange") @implements("arange", "function") def _arange(start, stop=None, step=None, dtype=None, *, like): args = [start, stop, step] From 61ba8a30bb402f150a82c51161bb6276784d267e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 19 Dec 2022 22:54:01 +0100 Subject: [PATCH 20/23] remove the outdated comment --- pint/facets/numpy/numpy_func.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 4cd8f1735..29dcdc95a 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -527,8 +527,6 @@ def _meshgrid(*xi, **kwargs): @implements("full_like", "function") def _full_like(a, fill_value, dtype=None, order="K", subok=True, shape=None): - # Make full_like by multiplying with array from ones_like in a - # non-multiplicative-unit-safe way if hasattr(fill_value, "_REGISTRY"): units = fill_value.units fill_value_ = fill_value.m @@ -981,8 +979,6 @@ def implementation(*args, like, **kwargs): @register_nep35_function("full") @implements("full", "function") def _full(shape, fill_value, dtype=None, order="C", *, like): - # Make full_like by multiplying with array from ones_like in a - # non-multiplicative-unit-safe way if hasattr(fill_value, "_REGISTRY"): units = fill_value.units fill_value_ = fill_value.m From 42df5ee620b7a5e860aa301cefe935d113da2c0d Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 19 Dec 2022 23:28:24 +0100 Subject: [PATCH 21/23] fix a few typos --- docs/user/numpy.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/numpy.ipynb b/docs/user/numpy.ipynb index 25866261b..2dc0ec99c 100644 --- a/docs/user/numpy.ipynb +++ b/docs/user/numpy.ipynb @@ -611,13 +611,13 @@ "source": [ "## Additional Comments\n", "\n", - "What follows is a short discussion about how NumPy support is implemented in Pint's `Quantity` Object.\n", + "What follows is a short discussion about how NumPy support is implemented in Pint's `Quantity` object.\n", "\n", "For the supported functions, Pint expects certain units and attempts to convert the input (or inputs). For example, the argument of the exponential function (`numpy.exp`) must be dimensionless. Units will be simplified (converting the magnitude appropriately) and `numpy.exp` will be applied to the resulting magnitude. If the input is not dimensionless, a `DimensionalityError` exception will be raised.\n", "\n", "In some functions that take 2 or more arguments (e.g. `arctan2`), the second argument is converted to the units of the first. Again, a `DimensionalityError` exception will be raised if this is not possible. ndarray or downcast type arguments are generally treated as if they were dimensionless quantities, whereas Pint defers to its declared upcast types by always returning `NotImplemented` when they are encountered (see above).\n", "\n", - "To achive these function and ufunc overrides, Pint uses the ``__array_function__`` and ``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that functions and ufuncs that Pint does not explicitly handle will error, rather than return a value with units stripped (in contrast to Pint's behavior prior to v0.10). For more\n", + "To achieve these function and ufunc overrides, Pint uses the ``__array_function__`` and ``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that functions and ufuncs that Pint does not explicitly handle will error, rather than return a value with units stripped (in contrast to Pint's behavior prior to v0.10). For more\n", "information on these protocols, see .\n", "\n", "This behaviour introduces some performance penalties and increased memory usage. Quantities that must be converted to other units require additional memory and CPU cycles. Therefore, for numerically intensive code, you might want to convert the objects first and then use directly the magnitude, such as by using Pint's `wraps` utility (see [wrapping](wrapping.rst)).\n", From 2d591a19c81c69bce6477d5422f2c988c3d30b2e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 19 Dec 2022 23:28:45 +0100 Subject: [PATCH 22/23] add some documentation on the implementation details of the changes --- docs/user/numpy.ipynb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/user/numpy.ipynb b/docs/user/numpy.ipynb index 2dc0ec99c..d6b1cd6c4 100644 --- a/docs/user/numpy.ipynb +++ b/docs/user/numpy.ipynb @@ -617,6 +617,10 @@ "\n", "In some functions that take 2 or more arguments (e.g. `arctan2`), the second argument is converted to the units of the first. Again, a `DimensionalityError` exception will be raised if this is not possible. ndarray or downcast type arguments are generally treated as if they were dimensionless quantities, whereas Pint defers to its declared upcast types by always returning `NotImplemented` when they are encountered (see above).\n", "\n", + "Array creation functions (including those that support the NEP35 `like` keyword argument) will return a quantity with the same unit / underlying array type as the input array. The only the exceptions to this pattern are:\n", + "- `full` and `full_like`, which return the same units as the `fill_value` keyword argument (or a non-quantity if the `fill_value` is not a quantity)\n", + "- `arange`, which returns the same units as `start`, `stop`, and `step`. More specifically, `np.arange(Q_(1, \"m\"), Q_(2, \"m\"), Q_(1, \"mm\"), like=Quantity(1, \"s\"))` is valid and returns an array with shape `(1000,)`.\n", + "\n", "To achieve these function and ufunc overrides, Pint uses the ``__array_function__`` and ``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that functions and ufuncs that Pint does not explicitly handle will error, rather than return a value with units stripped (in contrast to Pint's behavior prior to v0.10). For more\n", "information on these protocols, see .\n", "\n", From 78700de9c727e3d923fc71acccddd0b03553ee8e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 21 Dec 2022 23:38:29 +0100 Subject: [PATCH 23/23] replace unused parameters to `full_like` with `**kwargs` This is important because `dask` chose not to implement `subok`. --- pint/facets/numpy/numpy_func.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 11682329e..d2dd49df4 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -527,7 +527,7 @@ def _meshgrid(*xi, **kwargs): @implements("full_like", "function") -def _full_like(a, fill_value, dtype=None, order="K", subok=True, shape=None): +def _full_like(a, fill_value, **kwargs): if hasattr(fill_value, "_REGISTRY"): units = fill_value.units fill_value_ = fill_value.m @@ -535,9 +535,7 @@ def _full_like(a, fill_value, dtype=None, order="K", subok=True, shape=None): units = None fill_value_ = fill_value - magnitude = np.full_like( - a.m, dtype=dtype, order=order, subok=subok, shape=shape, fill_value=fill_value_ - ) + magnitude = np.full_like(a.m, fill_value=fill_value_, **kwargs) if units is not None: return fill_value._REGISTRY.Quantity(magnitude, units) else: