Repository navigation
Implemented np.allclose in numba/np/arraymath.py - #7940
Conversation
|
Seems like |
| a = np.asarray(a) | ||
| b = np.asarray(b) | ||
| if a.shape != b.shape: | ||
| if _can_broadcast_returns_bool(a, b.shape): | ||
| a = np.broadcast_to(a, b.shape) | ||
| elif _can_broadcast_returns_bool(b, a.shape): | ||
| b = np.broadcast_to(b, a.shape) | ||
| else: | ||
| raise ValueError('operands could not be broadcast together ' | ||
| 'with remapped shapes') |
There was a problem hiding this comment.
The broadcasting here fails if one of a or b is a scalar. The reason being, .shape will be ()(an empty UniTuple) and it will fail while JITing the code.
There was a problem hiding this comment.
As you notice, one cannot call np.broadcast_to with an empty shape. In the case one of the arguments is an integer, you can iterate over the array and check if they're the same:
if isinstance(b, types.Integer):
def impl(a, b, rtol=1e-05, atol=1e-08, equal_nan=False):
for av in np.nditer(a):
if not allclose_inner(av, b, rtol, atol, equal_nan):
return False
return True
return impl
else:
def np_allclose_impl(a, b, rtol=1e-05, atol=1e-08, equal_nan=False):
...
return np_allclose_implYou can replicate the idea above for the cases where
- both
aandbare scalars - either
aorbis scalar - both
aandbare arrays
There was a problem hiding this comment.
This sounds like a nice approach. Thanks for the idea.
|
@guilhermeleobas Could you please review this PR and provide your thoughts/opinions on the above comments? Thanks. |
guilhermeleobas
left a comment
There was a problem hiding this comment.
Hi @czgdp1807, thanks for the contribution to the Numba codebase. This is a nice addition. I've made a few comments regarding the usage of assert and answered your questions on how you can handle mixed input types (i.e. array and int).
Also, you can copy the inputs NumPy uses to test np.allclose to make sure we're covering all cases.
https://github.com/numpy/numpy/blob/aac965af6032b69d5cb515ad785cc9a331e816f4/numpy/core/tests/test_numeric.py#L2203-L2285
|
|
||
| a = np.asarray([1e10, 1e-7]) | ||
| b = np.asarray([1.00001e10, 1e-8]) | ||
| assert not cfunc(a, b) |
There was a problem hiding this comment.
Replace the assert by self.assertEqual.
There was a problem hiding this comment.
I though that since, output of cfunc and pyfunc is a bool object so using assert should be fine. Though I am fine with self.assertEqual as well. Will do the replacements.
| a = np.asarray([1e10, 1e-7]) | ||
| b = np.asarray([1.00001e10, 1e-8]) |
There was a problem hiding this comment.
You can aggregate the input values into a list (or tuple) and do the assertion inside the loop body:
pyfunc = np_allclose
cfunc = jit(nopython=True)(pyfunc)
inps = [
(np.asarray([1e10, 1e-7]), np.asarray([1.00001e10, 1e-8]),
(np.asarray([1e10, 1e-8]), np.asarray([1.00001e10, 1e-9]),
(np.asarray([1e10, 1e-8]), np.asarray([1.0001e10, 1e-9])),
(np.asarray([1e10]), np.asarray([1.0001e10, 1e-9])),
# ....
]
for a, b in inps:
expected = pyfunc(a, b)
got = cfunc(a, b)
self.assertEqual(expected, got)| a = np.asarray(a) | ||
| b = np.asarray(b) | ||
| if a.shape != b.shape: | ||
| if _can_broadcast_returns_bool(a, b.shape): | ||
| a = np.broadcast_to(a, b.shape) | ||
| elif _can_broadcast_returns_bool(b, a.shape): | ||
| b = np.broadcast_to(b, a.shape) | ||
| else: | ||
| raise ValueError('operands could not be broadcast together ' | ||
| 'with remapped shapes') |
There was a problem hiding this comment.
As you notice, one cannot call np.broadcast_to with an empty shape. In the case one of the arguments is an integer, you can iterate over the array and check if they're the same:
if isinstance(b, types.Integer):
def impl(a, b, rtol=1e-05, atol=1e-08, equal_nan=False):
for av in np.nditer(a):
if not allclose_inner(av, b, rtol, atol, equal_nan):
return False
return True
return impl
else:
def np_allclose_impl(a, b, rtol=1e-05, atol=1e-08, equal_nan=False):
...
return np_allclose_implYou can replicate the idea above for the cases where
- both
aandbare scalars - either
aorbis scalar - both
aandbare arrays
| with self.assertRaises(ValueError): | ||
| cfunc(a, b) |
There was a problem hiding this comment.
Move the test that raises an exception to its own function. If I'm not wrong, one need to disable the leak check when asserting exceptions:
def test_allcase_exception(self):
self.disable_leak_check()
a = np.asarray([1e10, 1e-9, np.nan])
b = np.asarray([1.0001e10, 1e-9])
with self.assertRaises(ValueError) as e:
cfunc(a, b)
self.assertIn("put here the expected exception msg", str(e.exception))|
|
||
| py_result = pyfunc(a + noise, a, atol=atol, rtol=rtol) | ||
| c_result = cfunc(a + noise, a, atol=atol, rtol=rtol) | ||
| assert py_result == c_result |
| assert not cfunc(a, b) | ||
| assert cfunc(a, b, equal_nan=True) |
There was a problem hiding this comment.
Replace the assert by self.assertEqual
|
|
||
| a = np.asarray([1e10, 1e-8]) | ||
| b = np.asarray([1.00001e10, 1e-9]) | ||
| assert cfunc(a, b) |
There was a problem hiding this comment.
Replace the assert by self.assertEqual.
|
|
||
| a = np.asarray([1e10, 1e-8]) | ||
| b = np.asarray([1.0001e10, 1e-9]) | ||
| assert not cfunc(a, b) |
There was a problem hiding this comment.
Replace the assert by self.assertEqual.
|
|
||
| a = np.asarray([1e10]) | ||
| b = np.asarray([1.0001e10, 1e-9]) | ||
| assert not cfunc(a, b) |
There was a problem hiding this comment.
Replace the assert by self.assertEqual.
|
@guilhermeleobas I have addressed the reviews and responded to the questions. |
guilhermeleobas
left a comment
There was a problem hiding this comment.
Good work @czgdp1807. I have a few more comments that should be easy to address.
| elif is_a_scalar and not is_b_scalar: | ||
| def np_allclose_impl_scalar_array(a, b, rtol=1e-05, atol=1e-08, | ||
| equal_nan=False): | ||
| b = np.asarray(b) | ||
| for bv in np.nditer(b): | ||
| if not _allclose_scalars(a, bv.item(), rtol=rtol, atol=atol, | ||
| equal_nan=equal_nan): | ||
| return False | ||
| return True | ||
| return np_allclose_impl_scalar_array |
There was a problem hiding this comment.
You can replace this block of code by:
| elif is_a_scalar and not is_b_scalar: | |
| def np_allclose_impl_scalar_array(a, b, rtol=1e-05, atol=1e-08, | |
| equal_nan=False): | |
| b = np.asarray(b) | |
| for bv in np.nditer(b): | |
| if not _allclose_scalars(a, bv.item(), rtol=rtol, atol=atol, | |
| equal_nan=equal_nan): | |
| return False | |
| return True | |
| return np_allclose_impl_scalar_array | |
| elif is_a_scalar and not is_b_scalar: | |
| def np_allclose_impl_scalar_array(a, b, rtol=1e-05, atol=1e-08, | |
| equal_nan=False): | |
| return np.allclose(b, a, rtol=rtol, atol=atol, equal_nan=equal_nan) | |
| return np_allclose_impl_scalar_array |
There was a problem hiding this comment.
I don't think this will give right results all the time, because the check depends on the ordering of a and b. For (a, b) the check is abs(a - b) > atol + rtol * abs(b), where as for (b, a) the check is abs(b - a) > atol + rtol * abs(a). See the RHS in both the inequalities, one uses b and the other uses a. So, np.allclose(a, b) might give different result from np.allclose(b, a) for edge cases.
Quoting from "Notes" section of https://numpy.org/doc/stable/reference/generated/numpy.allclose.html
The above equation is not symmetric in a and b, so that
allclose(a, b)might be different fromallclose(b, a)in some rare cases.
Please let me know if should still apply the above code suggestion.
There was a problem hiding this comment.
That's fine. I wasn't aware of this info.
| b = np.asarray([np.nan, 1.0]) | ||
| self.assertFalse(cfunc(a, b)) | ||
|
|
||
| # NumPy test data |
There was a problem hiding this comment.
Can you also include the link to NumPy repository pointing to the tests?
| b = np.asarray([np.nan, 1.0]) | ||
| self.assertFalse(cfunc(a, b)) | ||
|
|
||
| # NumPy test data |
There was a problem hiding this comment.
Maybe split the tests into different functions. This makes it easy to debug when there's a test failure.
| c_result = cfunc(a + noise, a, atol=atol, rtol=rtol) | ||
| self.assertEqual(py_result, c_result) | ||
|
|
||
| def test_allcase_exception(self): |
There was a problem hiding this comment.
| def test_allcase_exception(self): | |
| def test_allclose_exception(self): |
|
@guilhermeleobas @gmarkall I have addressed/responded to the new reviews and the tests pass. Please let me know if something else is to be done. |
guilhermeleobas
left a comment
There was a problem hiding this comment.
Thanks, @czgdp1807, I only have a few more requests.
| def test_allclose_numpy_data(self): | ||
|
|
||
| pyfunc = np_allclose |
There was a problem hiding this comment.
Can you split the content of this test case into different functions? Similar to what NumPy does.
| ] | ||
|
|
||
| for (x, y) in numpy_data: | ||
| self.assertTrue(cfunc(x, y)) |
There was a problem hiding this comment.
Instead of asserting to True, compare the outputs of cfunc(x, y) and pyfunc(x, y) are equals.
| ] | ||
|
|
||
| for (x, y) in numpy_data: | ||
| self.assertFalse(cfunc(x, y)) |
There was a problem hiding this comment.
Same here. Compare the outputs of cfunc(x, y) and pyfunc(x, y).
| raise TypeError('The first argument "b" must be array-like') | ||
|
|
||
| if not isinstance(rtol, types.Float): | ||
| raise TypeError('The fourth argument "rtol" must be a ' |
There was a problem hiding this comment.
rtol is the third argument, right?
| raise TypeError('The fourth argument "rtol" must be a ' | |
| raise TypeError('The third argument "rtol" must be a ' |
| 'floating point') | ||
|
|
||
| if not isinstance(atol, types.Float): | ||
| raise TypingError('The fifth argument "atol" must be a ' |
There was a problem hiding this comment.
| raise TypingError('The fifth argument "atol" must be a ' | |
| raise TypingError('The fourth argument "atol" must be a ' |
| 'floating point') | ||
|
|
||
| if not isinstance(equal_nan, types.Boolean): | ||
| raise TypeError('The sixth argument "equal_nan" must be a ' |
There was a problem hiding this comment.
| raise TypeError('The sixth argument "equal_nan" must be a ' | |
| raise TypeError('The fifth argument "equal_nan" must be a ' |
| self.assertFalse(cfunc(a, b)) | ||
| self.assertTrue(cfunc(a, b, equal_nan=True)) |
There was a problem hiding this comment.
Compare the output of cfunc with pyfunc instead.
| self.assertTrue(cfunc(a, b, equal_nan=True)) | ||
|
|
||
| b = np.asarray([np.nan, 1.0]) | ||
| self.assertFalse(cfunc(a, b)) |
There was a problem hiding this comment.
Compare the output of cfunc with pyfunc instead.
|
@czgdp1807, I just remembered that you need to update the file |
|
Sure. I will do that tomorrow. |
Done. |
guilhermeleobas
left a comment
There was a problem hiding this comment.
LGTM. Thanks, @czgdp1807
|
@sklam @stuartarchibald @esc Same for this PR. Is it ready for merge? Please let me know if anything is left to be done here. Thanks. |
sklam
left a comment
There was a problem hiding this comment.
This should be the last needed change
| raise TypeError('The first argument "a" must be array-like') | ||
|
|
||
| if not type_can_asarray(b): | ||
| raise TypeError('The first argument "b" must be array-like') |
There was a problem hiding this comment.
| raise TypeError('The first argument "b" must be array-like') | |
| raise TypeError('The second argument "b" must be array-like') |
There was a problem hiding this comment.
I have addressed the review and the tests are passing. Please let me know if anything else is to be done here. Thanks.
#4074
I am implementing
np.allcloseinnumba/np/arraymath.pyas I noticed that it hasn't been added yet and I was unable to find any open PR already working on it. Please let me know if its already there or someone recently is already working on it, I will close the PR then.I am working on adding unit test and therefore I added
[skip ci]in my commit so that my PR doesn't consume CI resources unnecessarily. Meanwhile if you have any comments/reviews for the implementation, please let me know. Thanks.