-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
ENH: add timedelta modulus operator support (mm) #12120
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
Conversation
Shouldn't the ufunc machinery be able to handle this itself? How is the other arithmetic handled? |
c472685
to
b662819
Compare
The ufunc machinery for timedelta arithmetic is here in |
I think it would definitely be preferred to implement remainder as another ufunc loop. Hard coding it into |
It seems that writing in a I can disable a check in I'll keep trying! |
A nice followup would be divmod, which can probably reuse the same code. |
You probably need to tweak the type resolver function |
Does it need to be added to numpy/core/code_generators/generate_umath.py? |
Apparently yes, and I'm likely going to have to write a TypeResolver function for remainder too. Looks like there's a fair bit of copy-pasting with As far as I can tell this isn't that different from what I've done in this PR -- the Python C API is still used for type checking and making decisions, the mess is just being hidden in the resolver machinery. |
Needs a release note. |
b662819
to
9aaeafa
Compare
Refactored to use ufunc machinery as requested -- it appears remainder now works for all combinations of My decision to typecast as: Points for:
Points against:
Also good if we can decide on what @shoyer does this need a mailing list check first maybe? Might be nice if we could confine this PR to |
As far as I'm concerned, mm->m is the only reasonable signature for modulus. If in doubt, look at the behavior of the builtin timedelta. |
from built-in
So maybe supporting modulus with int and float is more controversial (was suggested as expected to work in the linked issue). |
The codecov checks are green, but it didn't actually do anything -- missing a report or something. |
If I may clarify #12092: from
it follows that all three quantities (dividend, divisor × quotient, remainder) must be homogeneous and have the same time units. Since for multiplication (divisor × quotient) we have
i.e a numeric divisor should be accepted but the result should still be The
Therefore I find useful to define
while the corresponding
or
make no sense to me. In other terms: in euclidean division the quotient should be an integer, and this makes sense for |
@miccoli Ok, so timedelta stuff can be confusing, but in short -- we're in agreement for the currently proposed One point for possible clarification from your analysis:
This PR currently proposes allowing: That is type homogenous, but the units are not--are you suggesting we don't want that? |
I think this is arguably a bug, especially on Python 3 -- you should need to write I think there's a case for sticking with
I think this is the correct behavior.
Reproducing the behavior of |
I agree that this is correct.
For reference:
thus Therefore I would argue that In conclusion: I agree that |
I would argue that this is exactly what I would be in favor of deprecating |
|
||
return 0; | ||
|
||
type_reso_error: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This indent is a little jarring - I'd put the brace on its own line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to fix as #12147, I think
PyObject_Repr((PyObject *)PyArray_DESCR(operands[1]))); | ||
PyErr_SetObject(PyExc_TypeError, errmsg); | ||
Py_DECREF(errmsg); | ||
return -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is very exception-unsafe - you need to check for NULL from the result of PyObject_Repr
, PyUString_ConcatAndDel
, and PyUString_FromFormat
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Postponed as part of #12147
type_tup, out_dtypes); | ||
} | ||
if (type_num1 == NPY_TIMEDELTA) { | ||
if (type_num2 == NPY_TIMEDELTA) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just write this:
if (type_num1 == NPY_TIMEDELTA && type_num2 == NPY_TIMEDELTA) {
// your code
}
else {
return PyUFunc_DefaultTypeResolver(...)
}
That saves you from having to produce an error message for datetime, making all my above comments moot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is all just copy-paste from other type resolvers. I was planning to leave room for implementing mq
and md
remainders, where there would be other switches to handle type_num2
on a case by case basis, so check type_num1
but multiple checks on type_num2
.
@@ -1591,6 +1591,34 @@ TIMEDELTA_mm_d_divide(char **args, npy_intp *dimensions, npy_intp *steps, void * | |||
} | |||
} | |||
|
|||
NPY_NO_EXPORT void | |||
TIMEDELTA_mm_m_remainder(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function looks correct, thanks
9aaeafa
to
17237f5
Compare
Updated with a small reference doc example and to reflect the error handling code changes merged in from Eric recently. Also added a release note |
17237f5
to
0a807fe
Compare
numpy/core/src/umath/loops.c.src
Outdated
const npy_timedelta in1 = *(npy_timedelta *)ip1; | ||
const npy_timedelta in2 = *(npy_timedelta *)ip2; | ||
if (in1 == NPY_DATETIME_NAT || in2 == NPY_DATETIME_NAT) { | ||
*((npy_timedelta *)op1) = NPY_NAN; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we should propagate NaT
instead here to preserve the mm
->m
signature -- just noticing this as I try to round up the coverage % a little on the patch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
0a807fe
to
e07f7dc
Compare
Seems strange to me that Something for a later PR. |
@@ -119,6 +119,9 @@ simple datetime calculations. | |||
>>> np.timedelta64(1,'W') / np.timedelta64(1,'D') | |||
7.0 | |||
|
|||
>>> np.timedelta64(1, 'us') % np.timedelta64(727, 'ns') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have thought something like np.timedelta64(1,'W') % np.timedelta64(10,'D')
would be a slightly clearer example, but not really important
TD(intflt), | ||
[TypeDescription('m', FullTypeDescr, 'mm', 'm'), | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line wrapping here is a little weird, and doesn't match the other cases with only one TypeDescription
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nits, looks otherwise good.
numpy/core/tests/test_datetime.py
Outdated
# similar behavior enforced by CPython timedelta | ||
with assert_raises_regex(RuntimeWarning, | ||
"divide by zero encountered in remainder"): | ||
np.timedelta64(10, 's') % np.timedelta64(0, 's') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should check the result (0) too
e07f7dc
to
6461602
Compare
numpy/core/tests/test_datetime.py
Outdated
|
||
@pytest.mark.parametrize("val1, val2", [ | ||
# years and months can't be unambiguously | ||
# divided for modulus operation except for Y % M |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I means, strictly M % Y
, M % M
, Y % Y
are all fine too. There is nothing special about how Y and M behave - there are just rules prohibiting mixing units larger than W with units smaller than or equal to W. In isolation, all the units behave the same.
6461602
to
abca780
Compare
Cleaned up the test comment a bit & rebased / force pushed so we get a Windows test on appveyor for the time being. |
numpy/core/tests/test_datetime.py
Outdated
def test_timedelta_modulus_div_by_zero(self): | ||
# similar behavior enforced by CPython timedelta | ||
with assert_raises_regex(RuntimeWarning, | ||
"divide by zero encountered in remainder"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, why does this raise a warning? Shouldn't it warn a warning?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In CPython, ZeroDivisionError: integer division or modulo by zero
is raised by timedelta(seconds=10) % timedelta(seconds=0)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For development all warnings except a few are raised as errors in pytest.ini
, but in the absence of that file it should just be a warning. Is it actually raised by the code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eric is right -- in this feature branch it is just a warning when executed as plain code outside the test suite, so I should likely just check for a warning.
I assume we deviate from CPython timedelta because NumPy can gracefully handle division by 0 in scenarios where Python raises an exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully assert_warns
works then -- I'd normally use the pytest equivalent, but there's no precedent for that in NumPy IIRC.
numpy/core/tests/test_datetime.py
Outdated
with assert_raises_regex(RuntimeWarning, | ||
"divide by zero encountered in remainder"): | ||
actual = np.timedelta64(10, 's') % np.timedelta64(0, 's') | ||
assert_equal(actual, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coverage says this line is never hit, meaning the result in the array is never actually checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I wasn't sure why you asked me to check that the result is zero in a previous review comment, but now I'm seeing that you thought / think we should deviate from standard Python on this one and have warning instead of exception that break control flow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think something weird is going on within pytest that's promoting the warning to an error. I can divide by zero just fine:
In [13]: np.float64(1) % np.float64(0)
C:\Program Files\Python 3.5\Scripts\ipython:1: RuntimeWarning: invalid value encountered in double_scalars
Out[13]: nan
In [16]: np.int64(1) % np.int64(0)
C:\Program Files\Python 3.5\Scripts\ipython:1: RuntimeWarning: divide by zero encountered in longlong_scalars
Out[16]: 0
I think you need to use assert_warns
or something here, and then the warning will not escalate, and you can check the result too.
* added support for modulus operator with timedelta operands; type signature is mm->m
abca780
to
c9a6b02
Compare
Revised to switch a test from checking for exception to warning, as requested. |
Add support for modulus operator when both operands
are
timedelta64
with seconds units, and no other cases.Related to #12092, though doesn't fully cover the modulus
scenarios requested there because I haven't added a branch
for modulus
timedelta64
with a Python integer.I think this approach can be summarized as intercepting thearray
nb_remainder
slot function before it dispatches tothe ufunc machinery.