Repository navigation
Add isclose() method to Time and TimeDelta classes - #10646
Conversation
|
Hello @taldcroft 👋! It looks like you've made some changes in your pull request, so I've checked the code again for style. There are no PEP8 style issues with this pull request - thanks! 🎉 Comment last updated at 2020-10-05 12:50:18 UTC |
|
Hmm, I guess |
mhvk
left a comment
There was a problem hiding this comment.
Overall, looks good, but some comments. Most annoying is to uncover a bug... Though perhaps it is good that it is so rare for Time these days!
| raise TypeError("'atol' argument must be a Quantity or TimeDelta instance, got " | ||
| f'{atol.__class__.__name__} instead') | ||
|
|
||
| # Note: np.abs(self - other) results in an object array of TimeDelta |
There was a problem hiding this comment.
There should be no need to do that - TimeDelta should be smart enough! (And I think it is - if not, that's a bug...)
There was a problem hiding this comment.
I tried the original version first and it revealed the bug. I'll open an issue, but in the meantime the workaround code is needed.
In [2]: dt = TimeDelta([-1, 1]*u.s)
In [3]: np.abs(dt)
Out[3]:
array([<TimeDelta object: scale='None' format='jd' value=1.1574074074074073e-05>,
<TimeDelta object: scale='None' format='jd' value=1.1574074074074073e-05>],
dtype=object)
There was a problem hiding this comment.
As noted there, plain abs(...) should work.
| Default is one bit in the 128-bit JD time representation, equivalent | ||
| to about 20 picosecs. | ||
| """ | ||
| if not isinstance(other, Time): |
There was a problem hiding this comment.
I'm wondering whether the checks are necessary, since ideally the comparison just breaks. Indeed,
In [20]: TimeDelta(1*u.day) < Time.now()
TypeError: '<' not supported between instances of 'TimeDelta' and 'Time'
But very strangely,
In [21]: TimeDelta(1*u.day, scale='tai') < Time('J2000')
Out[21]: True
It looks like it has something to do with the specific scale picked - I'll investigate.
Ignoring that, would it be an idea to remove the checks? It might allow Time and TimeDelta can share the implementation.
There was a problem hiding this comment.
I favor a more verbose implementation that does input validation up front and gives a clear message about what is wrong. With your version the user is likely to not immediately know what the problem is. IMHO we need more explicit validation in astropy.
There was a problem hiding this comment.
I guess it is the usual "look before you leap" vs "easier to ask for forgiveness than permission" - https://docs.python.org/3/glossary.html#term-eafp
Generally, python seems to prefer the latter, and I think it is not illogical here either. E.g., suppose someone writes a different MyTime class (say, one that is integer + fraction in seconds), which they try to ensure can interact with Time, e.g., by defining subtraction with astropy's Time (which they can make work, because python will give their __rsub__ a chance after our __sub__ fails). If this also returns something that can have its absolute value taken and be compared with a quantity or TimeDelta (as they might well, since those are methods we've had for a long time), then the actual isclose() code will "just work". But in the current implementation, the isinstance check makes it impossible to ever work. (This is similar to numpy having np.asarray sprinkled throughout their code, making it not work with Quantity, and indeed np.abs sort-of working rather than just plain failing).
p.s. In principle, in EAFP, one does try/except and gives perhaps more informative error messages, but I've slowly moved to a state where I think one should just count on the user looking at the docstring when an error is raised. And in this case, one would generally get a quite informative TypeError from the subtraction anyway.
There was a problem hiding this comment.
I see your point and it is valid. OTOH, it happens frequently (based on real astropy issues I've seen) that users get confused because of an obscure error message raised in an unrelated bit of code when they pass a bad input. So there is a balance between addressing a problem that it almost sure to occur (bad inputs) vs. a new MyTime class that is less likely.
It's also worth noting that a major theme of recent Python development is type annotation. I've actually started paying some attention to this since there must be a reason the core devs are spending so much time on it. From this perspective we would annotate the input with something. Agreed that I'm not sure how this really works with duck-typing, which is certainly an incredibly useful concept.
I guess the middle ground is putting the main bit of the function in a try / except TypeError and re-raising with a better exception.
There was a problem hiding this comment.
Agreed that the MyTime class is not very likely, and I should add that this is not a big deal! But if one does it without anything, the type of exception one gets is already quite clear:
In [11]: class TTime(Time):
...: def isclose(self, other, atol=1*u.ns):
...: return abs(self - other)<atol
...:
...:
In [12]: t = TTime.now()
In [13]: dt = TimeDelta(100, format='jd')
In [14]: t.isclose('J2000')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-14-15f66b730fe3> in <module>
----> 1 t.isclose('J2000')
<ipython-input-11-b8b243443d6c> in isclose(self, other, atol)
1 class TTime(Time):
2 def isclose(self, other, atol=1*u.ns):
----> 3 return abs(self - other)<atol
4
5
TypeError: unsupported operand type(s) for -: 'TTime' and 'str'
In [15]: t.isclose(dt)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-15-f21ef40edf4f> in <module>
----> 1 t.isclose(dt)
<ipython-input-11-b8b243443d6c> in isclose(self, other, atol)
1 class TTime(Time):
2 def isclose(self, other, atol=1*u.ns):
----> 3 return abs(self - other)<atol
4
5
TypeError: bad operand type for abs(): 'TTime'
There was a problem hiding this comment.
p.s. The annotations are indeed intriguing. I think currently they are only to help writing correct code, not execution, but I think it is a good way to make the intent clearer. On annotations, our very own quantity_input decorator is an exception in that it actually uses annotations at runtime. Actually, that is seeing some nice discussion over at #10156 with a draft new implementation at #10655 - the latter might even get to your long-standing suggestion of having a way to ask for Quantity input that gives the function plain arrays in the right units! Would be good to get your input!
|
Is isclose a ufunc? If yes, does it make sense to create a WIP branch adding all the ufuncs we need that different people can contribute? I now see at least three issues / PRs that started implememting I usually don't like longer lived dev branches, but in this case it might be appropriate. |
bd628bb to
586f6f7
Compare
|
👍 to a separate branch to work on |
I still don't see that For |
For calendar dates (UT1, UTC, ...) I would agree. For timescales base on linear time offsets from an epoch, I think rtol would be well-defined. (Unix, TT, TAI, ...) |
|
Sure, for unix time there is technically a way to define Perhaps more importantly, I cannot see any practical use for |
586f6f7 to
98ea668
Compare
|
@taldcroft no, not really. Maybe unit testing of arithmetic with Time / TimeDelta? |
|
@mhvk - I think I have addressed all the points, including a compromise on duck-typing the input. The code that relies on |
That testing is already solid. I think that using |
I did not mean unit testing inside astropy but of packages using astropy, but you are probably right that there is no real use case here. |
|
@maxnoe - I'm a bit more with you on this, that Anyway, the method here is good and discoverable and definitely seems the best way to go for |
mhvk
left a comment
There was a problem hiding this comment.
Looks very nice. Two nitpicks, but OK to ignore, so approving.
|
@mhvk - thanks for the review. I thought about your comments for a bit and in the end I think for In general I still lean toward guaranteeing correctness with tests that run on the advertised API data types, rather than hoping for correctness based only on not generating an exception. Understood this is a big fuzzy line that we'll continually be straddling. |
Description
This pull request is to add an
isclose()method to theTimeandTimeDeltaclasses to make it easier to compare times to within a specified tolerance. The default tolerance is basically 1-bit in the mantissa of a float64 (np.finfo(float).eps= 2.2e-16 days or about 20 picosec), but theatolargument can be provided to be less strict.This partially addresses #8742.