Skip to content

Implemented np.allclose in numba/np/arraymath.py - #7940

Merged
sklam merged 16 commits into
numba:mainfrom
czgdp1807:np_allclose
Apr 21, 2022
Merged

sklam merged 16 commits into
numba:mainfrom
czgdp1807:np_allclose

Conversation

@czgdp1807

Copy link
Copy Markdown
Contributor

#4074

I am implementing np.allclose in numba/np/arraymath.py as 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.

@czgdp1807

Copy link
Copy Markdown
Contributor Author

Seems like [skip ci] didn't work.

Comment thread numba/np/arraymath.py Outdated
Comment thread numba/tests/test_np_functions.py Outdated
Comment thread numba/np/arraymath.py Outdated
Comment on lines +921 to +930
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')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_impl

You can replicate the idea above for the cases where

  • both a and b are scalars
  • either a or b is scalar
  • both a and b are arrays

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like a nice approach. Thanks for the idea.

@czgdp1807
czgdp1807 marked this pull request as ready for review April 1, 2022 11:41
@czgdp1807

Copy link
Copy Markdown
Contributor Author

@guilhermeleobas Could you please review this PR and provide your thoughts/opinions on the above comments? Thanks.

@guilhermeleobas guilhermeleobas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread numba/tests/test_np_functions.py Outdated

a = np.asarray([1e10, 1e-7])
b = np.asarray([1.00001e10, 1e-8])
assert not cfunc(a, b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the assert by self.assertEqual.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread numba/tests/test_np_functions.py Outdated
Comment on lines +3184 to +3185
a = np.asarray([1e10, 1e-7])
b = np.asarray([1.00001e10, 1e-8])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment thread numba/np/arraymath.py Outdated
Comment on lines +921 to +930
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')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_impl

You can replicate the idea above for the cases where

  • both a and b are scalars
  • either a or b is scalar
  • both a and b are arrays

Comment thread numba/np/arraymath.py Outdated
Comment thread numba/tests/test_np_functions.py Outdated
Comment on lines +3202 to +3203
with self.assertRaises(ValueError):
cfunc(a, b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Comment thread numba/tests/test_np_functions.py Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same.

Comment thread numba/tests/test_np_functions.py Outdated
Comment on lines +3207 to +3208
assert not cfunc(a, b)
assert cfunc(a, b, equal_nan=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the assert by self.assertEqual

Comment thread numba/tests/test_np_functions.py Outdated

a = np.asarray([1e10, 1e-8])
b = np.asarray([1.00001e10, 1e-9])
assert cfunc(a, b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the assert by self.assertEqual.

Comment thread numba/tests/test_np_functions.py Outdated

a = np.asarray([1e10, 1e-8])
b = np.asarray([1.0001e10, 1e-9])
assert not cfunc(a, b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the assert by self.assertEqual.

Comment thread numba/tests/test_np_functions.py Outdated

a = np.asarray([1e10])
b = np.asarray([1.0001e10, 1e-9])
assert not cfunc(a, b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the assert by self.assertEqual.

@czgdp1807

Copy link
Copy Markdown
Contributor Author

@guilhermeleobas I have addressed the reviews and responded to the questions.

@esc esc added 4 - Waiting on reviewer Waiting for reviewer to respond to author and removed 2 - In Progress labels Apr 4, 2022
@stuartarchibald stuartarchibald added the Effort - medium Medium size effort needed label Apr 5, 2022

@guilhermeleobas guilhermeleobas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work @czgdp1807. I have a few more comments that should be easy to address.

Comment thread numba/np/arraymath.py
Comment on lines +924 to +933
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can replace this block of code by:

Suggested change
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from allclose(b, a) in some rare cases.

Please let me know if should still apply the above code suggestion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine. I wasn't aware of this info.

Comment thread numba/tests/test_np_functions.py Outdated
b = np.asarray([np.nan, 1.0])
self.assertFalse(cfunc(a, b))

# NumPy test data

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also include the link to NumPy repository pointing to the tests?

Comment thread numba/tests/test_np_functions.py Outdated
b = np.asarray([np.nan, 1.0])
self.assertFalse(cfunc(a, b))

# NumPy test data

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe split the tests into different functions. This makes it easy to debug when there's a test failure.

Comment thread numba/tests/test_np_functions.py Outdated
c_result = cfunc(a + noise, a, atol=atol, rtol=rtol)
self.assertEqual(py_result, c_result)

def test_allcase_exception(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_allcase_exception(self):
def test_allclose_exception(self):

@gmarkall gmarkall added 4 - Waiting on author Waiting for author to respond to review and removed 4 - Waiting on reviewer Waiting for reviewer to respond to author labels Apr 6, 2022
@czgdp1807

Copy link
Copy Markdown
Contributor Author

@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.

@gmarkall gmarkall added 4 - Waiting on reviewer Waiting for reviewer to respond to author and removed 4 - Waiting on author Waiting for author to respond to review labels Apr 7, 2022

@guilhermeleobas guilhermeleobas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @czgdp1807, I only have a few more requests.

Comment thread numba/tests/test_np_functions.py Outdated
Comment on lines +3242 to +3244
def test_allclose_numpy_data(self):

pyfunc = np_allclose

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you split the content of this test case into different functions? Similar to what NumPy does.

Comment thread numba/tests/test_np_functions.py Outdated
]

for (x, y) in numpy_data:
self.assertTrue(cfunc(x, y))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of asserting to True, compare the outputs of cfunc(x, y) and pyfunc(x, y) are equals.

Comment thread numba/tests/test_np_functions.py Outdated
]

for (x, y) in numpy_data:
self.assertFalse(cfunc(x, y))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Compare the outputs of cfunc(x, y) and pyfunc(x, y).

Comment thread numba/np/arraymath.py Outdated
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 '

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rtol is the third argument, right?

Suggested change
raise TypeError('The fourth argument "rtol" must be a '
raise TypeError('The third argument "rtol" must be a '

Comment thread numba/np/arraymath.py Outdated
'floating point')

if not isinstance(atol, types.Float):
raise TypingError('The fifth argument "atol" must be a '

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise TypingError('The fifth argument "atol" must be a '
raise TypingError('The fourth argument "atol" must be a '

Comment thread numba/np/arraymath.py Outdated
'floating point')

if not isinstance(equal_nan, types.Boolean):
raise TypeError('The sixth argument "equal_nan" must be a '

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise TypeError('The sixth argument "equal_nan" must be a '
raise TypeError('The fifth argument "equal_nan" must be a '

Comment thread numba/tests/test_np_functions.py Outdated
Comment on lines +3204 to +3205
self.assertFalse(cfunc(a, b))
self.assertTrue(cfunc(a, b, equal_nan=True))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compare the output of cfunc with pyfunc instead.

Comment thread numba/tests/test_np_functions.py Outdated
self.assertTrue(cfunc(a, b, equal_nan=True))

b = np.asarray([np.nan, 1.0])
self.assertFalse(cfunc(a, b))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compare the output of cfunc with pyfunc instead.

@guilhermeleobas

guilhermeleobas commented Apr 8, 2022 •

Copy link
Copy Markdown
Contributor

@czgdp1807, I just remembered that you need to update the file numpysupported.rst to include np.allclose.

@czgdp1807

Copy link
Copy Markdown
Contributor Author

Sure. I will do that tomorrow.

@czgdp1807

Copy link
Copy Markdown
Contributor Author

I just remembered that you need to update the file numpysupported.rst to include np.allclose.

Done.

@guilhermeleobas guilhermeleobas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks, @czgdp1807

@czgdp1807

Copy link
Copy Markdown
Contributor Author

@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 sklam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be the last needed change

Comment thread numba/np/arraymath.py Outdated
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')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise TypeError('The first argument "b" must be array-like')
raise TypeError('The second argument "b" must be array-like')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have addressed the review and the tests are passing. Please let me know if anything else is to be done here. Thanks.

@sklam sklam added 4 - Waiting on author Waiting for author to respond to review and removed 4 - Waiting on reviewer Waiting for reviewer to respond to author labels Apr 14, 2022

@sklam sklam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the patch!

@sklam sklam added 5 - Ready to merge Review and testing done, is ready to merge and removed 4 - Waiting on author Waiting for author to respond to review labels Apr 20, 2022
@sklam
sklam merged commit 63bb6c3 into numba:main Apr 21, 2022
@stuartarchibald stuartarchibald added this to the Numba 0.56 RC milestone Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to merge Review and testing done, is ready to merge Effort - medium Medium size effort needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants