Skip to content

ENH: Make numpy floor_divide and remainder agree with Python // and %. #7258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 21, 2016

Conversation

charris
Copy link
Member

@charris charris commented Feb 16, 2016

This fixes the problems with divmod in #7224 and brings divmod for numpy float64 scalars and arrays into agreement with divmod for Python floats. Tests are added to enforce the new behavior and assures that the results for small integers scaled by powers of two are exact.

Too keep the scalar and array math in sync, both the scalar math and loop code is based on a new npy_divmod function in npy_math.

EDIT: I changed the name to npy_divmod.

divmod example, Python floats

    In [1]: a = 78 * 6e-8

    In [2]: b = 6e-8

    In [3]: divmod(a, b)
    Out[3]: (77.0, 5.999999999999965e-08)

Before this commit numpy float64 gave

    In [4]: divmod(float64(a), float64(b))
    Out[4]: (78.0, 0.0)

After this commit numpy float64 gives

    In [4]: divmod(float64(a), float64(b))
    Out[4]: (77.0, 5.9999999999999651e-08)

@charris charris added this to the 1.11.0 release milestone Feb 16, 2016
@charris charris force-pushed the python-compatible-floor_divide branch from 3fb9621 to c3ee1f9 Compare February 16, 2016 03:07
@charris
Copy link
Member Author

charris commented Feb 16, 2016

Note that pymodf uses fmod, as does Python. That function alone takes 4x longer to run than the old code :( That seems to be the price for a little bit more accuracy).

@charris charris force-pushed the python-compatible-floor_divide branch 3 times, most recently from fb2bd32 to dc9d6ed Compare February 16, 2016 17:42
if (div) {
floordiv = npy_floor(div);
if (div - floordiv > 0.5)
floordiv += 1.0;
Copy link
Member

Choose a reason for hiding this comment

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

0.5@c@, 1.0@c@ ? Probably doesn't matter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

The new function is taken from the Python version of float_divmod and
computes the result of floor_division and modulus together so that they
can be kept compatible. This should also result in the '//' and '%'
operators applied to np.float64 and Python float returning the same
values.

The intent is to implement the ufuncs floor_divide and remainder using
the npy_divmod so that their results will also match those of '//' and
'%' while providing consistency between the two.

Note that npy_divmod uses fmod, which is very slow. As a result, the
floor_division and remainder functions are about 4x slower than the
previous versions based on the floor function, but should be more
accurate.
The following numpy scalar floating functions are reimplemented
using the npy_divmod function.

- remainder ('%')
- floor_division ('//')
- divmod

Note that numpy scalars warn on zero division rather than raise.

divmod example, Python floats

    In [1]: a = 78 * 6e-8

    In [2]: b = 6e-8

    In [3]: divmod(a, b)
    Out[3]: (77.0, 5.999999999999965e-08)

Before this commit numpy float64 gave

    In [4]: divmod(float64(a), float64(b))
    Out[4]: (78.0, 0.0)

After this commit numpy float64 gives

    In [4]: divmod(float64(a), float64(b))
    Out[4]: (77.0, 5.9999999999999651e-08)
The floor function is no longer needed in scalarmath as its use has been
replaced by the new pymodf function.
The following numpy ufuncs are reimplemented using the npy_divmod
function.

- remainder ('%')
- floor_divide ('//')

Example of differences

Currently

    In [1]: a = np.array(78 * 6e-8)

    In [2]: b = np.array(6e-8)

    In [3]: a // b
    Out[3]: 77.0

    In [4]: a % b
    Out[4]: 5.9999999999999651e-08

Previously

    In [1]: a = np.array(78 * 6e-8)

    In [2]: b = np.array(6e-8)

    In [3]: a // b
    Out[3]: 78.0

    In [4]: a % b
    Out[4]: 0.0

The first agrees with the Python values for the same operation and is a
bit more accurate for the input values as represented internally.

Closes numpy#7224.
@charris charris force-pushed the python-compatible-floor_divide branch from dc9d6ed to bb05b60 Compare February 19, 2016 05:04
@pv
Copy link
Member

pv commented Feb 19, 2016

I think it should be clarified what the exact (in floating point) relation between floor_divide and remainder is, if it is known, and reflect that in the tests in the roundoff cases, in case such invariant is known. In cases where it's known that rounding error can exist, assert_almost_equal_nulp should probably be used instead of assert_allclose, because the default tolerance of the latter is fairly lax.

Add tests for some corner cases involving inf, zero, and nan.
Check that small integers are handled exactly.
The floor_divide (//) and remainder (%) functions are complementary
in the sense that a ~= (a % b) + (a // b).
@charris charris force-pushed the python-compatible-floor_divide branch from bb05b60 to 542c88b Compare February 19, 2016 16:26
@charris
Copy link
Member Author

charris commented Feb 19, 2016

Updated.

@charris
Copy link
Member Author

charris commented Feb 20, 2016

@pv To give an example where equality fails, suppose a = -1e-50, b = 1.0, fmod will give an accurate result of -1e-50, but in conversion to the unsymmetric Python form the integer part is determined to be -1 while the fractional part is (-e-50 + 1.0) == 1.0. Those rounding issues are why the remainder can only be determined to be in a closed interval rather than an half open one as the mathematics would require. That situation is covered in the corner cases test.

charris added a commit that referenced this pull request Feb 21, 2016
ENH: Make numpy floor_divide and remainder agree with Python `//` and `%`.
@charris charris merged commit 81db485 into numpy:master Feb 21, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants