Allow representations to contain differentials#6169
Conversation
|
(As a warning to others, @adrn and I have been doing a lot of out-of-band discussion about this so it might be difficult to follow the doc...) On the questions:
I'd vote that if there are no differentials, it should look just like the current version. E.g. (for comparison with the others below, If differentials are present, the most straightforward option would be: which is pretty trivial to implement. I could also see an argument for this, though: e.g., don't show the actual data, but expect the user to do
Yes, I like just what you suggested here. But be sure to add a
Hmm, there is an alternative: have |
|
@eteq First two points: sounds good! On the |
|
@adrn: the commits I just pushed up add the One other thing: I noticed the machinery for adding differentials is on |
|
@eteq ah, yea, I agree - I just moved the relevant code to |
|
@adrn, @eteq - only high-level comments and questions in the first instance. From highest to lowers:
|
|
|
@mhvk: On 2. I disagree that we're doing a "quick hack" for LTS. We've been thinking about this for over a year now and its time to get something in to support velocities. The feature freeze is sort of acting as a stick so that the perfect isn't the enemy of the good. I also think it's not unreasonable to have this release include a warning in the docs that says "this might change in future versions" which is fairly standard for big changes like this. As for whether there should be a container class with both representations and differentials, that was @adrn's initial idea, but I think I convinced him otherwise on the basis that this would be a major a API-breaking change, and @adrn realized it would be a lot of work with a variety of subtle consequences to change On 3. Good point... We ended up with the mix-and-match option because as long as its attached to the representation, the differential can always be re-cast into whatever representation is desired, so one can always get whatever one wants. But sometimes you don't want to. E.g. if you have a cartesian representation and differential and just want to get the (I'm ambivalent on 4) |
bc08ff6 to
2725e80
Compare
… along the differentials
… a representation
mhvk
left a comment
There was a problem hiding this comment.
This looks very good. I've many small comments -- except the expected key (which is probably an oversight), which I'll push a PR for.
Also, I did not change __repr__ but would suggest including the differential keys.
(So, hopefully all my comments that are addressed will be hidden soon.)
| arrstr = _array2string(self._values, prefix=prefixstr) | ||
|
|
||
| diffstr = '' | ||
| if getattr(self, 'differentials', None): |
There was a problem hiding this comment.
@adrn - I very much liked your idea of listing the keys of the differentials as a minimal indication of what was present. Any reason you abandoned it?
| ``recommended_units`` dictionary, which maps component names to the units | ||
| they are best presented to users in (this is used only in representations | ||
| of coordinates, and may be overridden by frame classes). | ||
| define a ``to_cartesian`` method and a ``from_cartesian`` |
There was a problem hiding this comment.
There doesn't seem to be any difference with the original any more; just revert?
|
|
||
| super(BaseRepresentation, self).__init__(*args, **kwargs) | ||
|
|
||
| # store as a private name so users know this is not settable |
There was a problem hiding this comment.
I would just remove the comment here; I think this is more confusing than elucidating!
|
|
||
| expected_key = diff._get_deriv_key(self) | ||
|
|
||
| if not isinstance(diff, BaseDifferential): |
There was a problem hiding this comment.
This can never be true, since we know here that diff.__class__ in self._compatible_differentials (which makes me wonder about subclasses, but fine to ignore that for now!). Just remove?
| "set to a string representation of the unit of " | ||
| "the derivative. Got '{0}'".format(type(diff))) | ||
|
|
||
| elif key != expected_key: |
There was a problem hiding this comment.
This perhaps is too strict, but I'm happy to start strict and possibly relax later...
| # TODO: better validation by checking, e.g., _compatible_differentials | ||
| # for this class | ||
| self._check_base(base) | ||
|
|
There was a problem hiding this comment.
I think this may indeed be too strict, but we can revisit if it turns out to be a problem.
| if d_comp: | ||
| d_unit = comp.si.unit / d_comp.si.unit | ||
| return str(d_unit) | ||
|
|
There was a problem hiding this comment.
I think this is too inprecise:
str(u.deg.si/(u.mas/u.yr).si)
# '1.13607e+14 s'
I think we just want the SI base and power. The easiest is a bit of a trick, but it works:
str(u.Quantity(1., comp.unit / d_comp.unit).si.unit)
str(u.Quantity(1., u.deg/(u.mas/u.yr**2)).si.unit)
# 's2'
| d_z=3 * u.km/u.s) | ||
|
|
||
| # can pass in a single differential, which gets turned into a | ||
| # length-1 tuple |
|
|
||
| r2 = CartesianRepresentation.from_representation(r1) | ||
| assert r2.get_name() == 'cartesian' | ||
| # assert r2.differentials['s'].get_name() == 'cartesian' |
| def test_to_cartesian(): | ||
| """ | ||
| Test that to_cartesian does the expected thing (converts both the | ||
| representation and differentials) |
|
OK, pushed my changes, including a second commit with also I would suggest that if we want to have a method that transforms differentials to cartesian as well, we discuss that in another PR, so that we can merge this one. |
|
Yes, I'm 👍 on merging. This has become a really nice integration of the differentials!! Next up is #6218, correct? That one I think was very close as it was; hopefully the rebase will not be too painful. |
|
Yes, next is #6218 - talk to you over there! |
|
(Remote data build failures) |
eteq
left a comment
There was a problem hiding this comment.
Looks good except for the missing docstrings about differentials and the bit about what repr should look like. If it seems like more debate is likely on the latter, can also merge this and deal with that as a separate small PR.
I do think somewhere (the docstrings or the narrative docs, or both), there needs to be a clear description of what the keys of differentials mean. That's not very intuitive at first glance (although I think it does make sense in its way)
CHANGES.rst
Outdated
| coordinate frames that allow copying an existing frame object with various | ||
| reference or copy behaviors and possibly overriding frame attributes. [#6182] | ||
|
|
||
| - The ``Representation`` class instances can now contain ``Differential`` |
There was a problem hiding this comment.
I think these shouldn't be in backticks because they're not actual class names... Could either do BaseRepresentation in backticks or just leave off the backticks and lower-case them to not be referncing sepcific class names
There was a problem hiding this comment.
Oops - I had meant to do *Representation but I'll just switch to plain text, lowercase.
| ('z', u.Quantity)]) | ||
|
|
||
| def __init__(self, x, y=None, z=None, unit=None, xyz_axis=None, copy=True): | ||
| def __init__(self, x, y=None, z=None, unit=None, xyz_axis=None, |
There was a problem hiding this comment.
The docstring doesn't have "differnentials" mentioned...
| return SphericalRepresentation | ||
|
|
||
| def __init__(self, lon, lat, copy=True): | ||
| def __init__(self, lon, lat, differentials=None, copy=True): |
There was a problem hiding this comment.
again, no differentials in the docstring
| _unit_representation = UnitSphericalRepresentation | ||
|
|
||
| def __init__(self, lon, lat, distance, copy=True): | ||
| def __init__(self, lon, lat, distance, differentials=None, copy=True): |
|
|
||
| def __init__(self, phi, theta, r, copy=True): | ||
| def __init__(self, phi, theta, r, differentials=None, copy=True): | ||
| super(PhysicsSphericalRepresentation, |
|
|
||
| def __init__(self, distance, copy=True): | ||
| super(RadialRepresentation, self).__init__(distance, copy=copy) | ||
| def __init__(self, distance, differentials=None, copy=True): |
|
|
||
| def __init__(self, rho, phi, z, copy=True): | ||
| def __init__(self, rho, phi, z, differentials=None, copy=True): | ||
| super(CylindricalRepresentation, |
|
|
||
| recommended_units = {} # subclasses can override | ||
|
|
||
| def __init__(self, *args, **kwargs): |
There was a problem hiding this comment.
More debatable here, but it would probably be useful for this to have a docstring of what's valid for differentials here
| diffstr = '' | ||
| if getattr(self, 'differentials', None): | ||
| diffstr = '\n (has differentials: {0})'.format( | ||
| ', '.join([repr(key) for key in self.differentials.keys()])) |
There was a problem hiding this comment.
I find the description here confusing because it implies the differentials have that unit. How about "has differentials: d/d[s]" or something like that to make it clear its the derivative unit? I think this might do it:
', '.join(['d/d[{}]'.format(key) for key in ks])
There was a problem hiding this comment.
Yea, this is why I ended up agreeing with you about just showing 'has differentials'. What is shown now are the keys to the differentials dictionary. I'm not sure how to get rid of the ambiguity you mention - I worry that if we do d/ds people might get confused about what the actual key is. I think we either do something like has differentials with keys=['s', 's2'] or just go back to has differentials
There was a problem hiding this comment.
cc @mhvk FYI: this comment is related to why I had removed exposing the units from the __repr__
There was a problem hiding this comment.
It's clear there's not consensus here, so I'm ok with merging as it stands on this issue and will make a separate small PR to make the change I suggested.
|
One more suggestion after some out-of-band questioning with @adrn : there should be a test to ensure that the following is not accepted as a differential |
|
The new test I added passes locally, but since I added quite a few lines of documentation I am trying out the new |
|
Hm, it seems that the |
|
Hmm, what do you mean not working? I think it was: https://travis-ci.org/astropy/astropy/jobs/244281543#L381 (Sadly we can't avoid to start up the jobs, but can just cancel them before any conda install and the testing happens. So by no means cancelling them manually is more efficient but less convenient). |
|
Oh, ignore my comment! I saw the jobs spinning up and jumped to conclusions. I take it back! |
|
I'm satisfied, so when the doc tests pass feel free to pull the trigger @adrn . You've definitely earned that! 💯 |
|
Doh - the docs build failure is because of 8 warnings about mistaken single backticks I had in the docstrings I added. I fixed and ran |
|
Thanks all for the review -- moving on to #6218 |
|
🚀 🤘 👏 🔁 |
This is a stepping stone PR towards adding support for proper motions and velocities in
astropy.coordinates. It turns out, we need to either (1) allowRepresentations to carry aroundDifferentials, or (2) create a new container class for representation+differentials. Option 2 seems better from a design perspective, but would lead to some API-breaking changes (or else a lot of really annoying code to provide backwards-compatibility). As this only really affects the lower-level API (and not theFrameorSkyCoord-level API), we decided that, in the interest of time, we would make this "hack" for now and just clearly document that the low-level API could change.This PR adds the necessary code to allow
Representation's to accept any number ofDifferential's in their initializers. For simplicity, we are very strict:Differentialinstances must have the same shape as the representation. (I could see loosening this a little and just requiring that they are broadcastable on initialization, but leave that as an open question)_apply()-related operations) affect both the representation and the differentialsCheck out this document if you're interested in seeing a very messy document that lays out some more details: https://docs.google.com/document/d/1dZIG7CG8h-C4lcegtx71eUc76K5LHtgRBsXk4iRa2ds/edit?usp=sharing