Skip to content

Allow representations to contain differentials#6169

Merged
adrn merged 56 commits intoastropy:masterfrom
adrn:coordinates/diffs-on-reps
Jun 18, 2017
Merged

Allow representations to contain differentials#6169
adrn merged 56 commits intoastropy:masterfrom
adrn:coordinates/diffs-on-reps

Conversation

@adrn
Copy link
Member

@adrn adrn commented Jun 8, 2017

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) allow Representations to carry around Differentials, 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 the Frame or SkyCoord-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 of Differential's in their initializers. For simplicity, we are very strict:

  • The Differential instances 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)
  • Shape-changing (e.g., _apply()-related operations) affect both the representation and the differentials

Check 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

@adrn adrn added this to the v2.0.0 milestone Jun 8, 2017
@adrn adrn requested review from eteq and mhvk June 8, 2017 23:56
@eteq
Copy link
Member

eteq commented Jun 9, 2017

(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:

How do we modify the repr() on BaseRepresentation when there are differentials associated?

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,

<CartesianRepresentation (x, y, z) in kpc
    ( 0.0663711,  0.75566441, -1.29113804)>

If differentials are present, the most straightforward option would be:

<CartesianRepresentation (x, y, z) in kpc
    ( 0.0663711,  0.75566441, -1.29113804),
 differentials=(<CartesianDifferential (d_x, d_y, d_z) in kpc
    ( 1.82124186,  1.48662901, -1.21030373)>,)>

which is pretty trivial to implement.

I could also see an argument for this, though:

<CartesianRepresentation (x, y, z) in kpc
    ( 0.0663711,  0.75566441, -1.29113804),
 differentils=(CartesianDifferential(...), )>

e.g., don't show the actual data, but expect the user to do .differentials if they want to see the data. That prevents the repr from getting awkwardly long if there's lots of differentials. I think I slightly favor this over the above, but either seem ok.

Where should I document the subtleties of this? We want to document it somewhere, but also we don't really want people to use it explicitly, or at least have a clear warning on top. Should I just add a section to docs/coordinates/representations.rst near the bottom?

Yes, I like just what you suggested here. But be sure to add a .. warning:: this code is experimental and may change in future versions! at the top of the section.

One slightly annoying side effect of this is that any subclass of BaseRepresentation has to remember to pass through the differentials in to_cartesian() and from_cartesian() (see examples in this PR, e.g., SphericalRepresentation. Should we just accept that and document it?

Hmm, there is an alternative: have to_cartesian/from_cartesian be on BaseRepresentation and have the subclasses instead implement _real_to_cartesian/_real_from_cartesian, which does not ever include the differentials in its result. Then BaseRepresentation.to_cartesian would be something like:

def to_cartesian(self):
    repr = self._real_to_cartesian()
    repr._differentials = self._differentials 
    return repr

@adrn
Copy link
Member Author

adrn commented Jun 9, 2017

@eteq First two points: sounds good!

On the to_cartesian/from_cartesian idea: I was thinking something similar. My idea was to have the subclasses implement (something like) _to_cartesian_components(), which returns an OrderedDict with just the x, y, z data, and _from_cartesian_components(), which returns an OrderedDict with keys set to the component names of the given representation. I guess I was thinking about moving the actual creation of the object up to the superclass (since right now, they all do copy=False in the initializer as well.)

@eteq
Copy link
Member

eteq commented Jun 9, 2017

@adrn: the commits I just pushed up add the with_differential method. The last of the three actually does something different: it explicitly addresses passing in a string to represent_as. That wasn't being accounted for, but on reflection I think it's best to do this explicitly so as to yield a more helpful error message. But if you disagree feel free to back that out or do something different.

One other thing: I noticed the machinery for adding differentials is on BaseRepresentationOrDifferential, not BaseRepresentation. I think that's not what we want: differentials attached to other differentials creates an ambiguity: if I have an acceleration, should it be rep.differential[0].differential[0], or rep.differential[0]? Also, it's impossible to do the re-representations anyway if you don't have the representation and all the lower-order differentials. So I think differentials should only be attachable to representations, not other differentials. (Note that if you change this, you'll also need to move the BaseRepresentationOrDifferential._validate_differentials method which I added in those last few commits.)

@adrn
Copy link
Member Author

adrn commented Jun 9, 2017

@eteq ah, yea, I agree - I just moved the relevant code to BaseRepresentation locally. Will push some more changes shortly.

@mhvk
Copy link
Contributor

mhvk commented Jun 9, 2017

@adrn, @eteq - only high-level comments and questions in the first instance. From highest to lowers:

  1. Question: Why is this best done inside representations rather than at the frame level? I ask mostly because it makes sense for coordinates to have just RV or PM, but not really as much for representations, which suggests that this doesn't belong in representations.
  2. Comment: I do think we do need to get this right in one go: to have a "hack" just ahead of a LTS version means that we're stuck with the hack. Better it seems to bite the bullet and go for a design where we create a new class that holds both a representation and a differential. (I doubt that there are that many implementations of custom representations that we would break at this point.) [Badly thought out idea: could the new ones have the old names, and the old representations get new names? Does this even help?]
  3. Comment: from some of the tests, it seemed mix-and-match representations and differentials were allowed (spherical and cylindrical); is this the case and if so was this indeed intended? I would suggest that for every case, it should be possible to use the differential with as a base the representation it is attached to (i.e., without further converting the base). So, a spherical representation could hold spherical, unispherical and radial differential (plus coslat variants), but a cylindrical representation only a cylindrical differential.
  4. Comment: I feel operations on representations should not drop the differential but rather just error. (This allows them to start working with differentials later, for cases where that makes sense.)

@adrn
Copy link
Member Author

adrn commented Jun 9, 2017

@mhvk @eteq

  1. My recollection is that when I looked into this idea, I decided that it would end up requiring a lot of detailed changes to the frame classes and duplicated code because of some unfortunate choices in the development of the frames. Right now, the representations are stored as ._data on the frame classes (ugh), and most methods work with this attribute alone. We'd have to modify everything to also look for ._differentials (or whatever). @eteq and I briefly discussed the possibility of creating a new container class for representations and differentials that would replace ._data, but we're really down to the wire here and trying to carve out a minimal path towards getting something in before the feature freeze.

  2. I just don't think we have the time (or at least I don't) before next weekend. I do agree in principle and think that it would be the cleaner way to go, but I worry about the required fixes and downstream changes we'd need to make elsewhere for any API-breaking changes we make to the representations. Our compromise was to add the minimal functionality we needed and document it as experimental; wouldn't that mean we don't need to provide LTS for this functionality? In any case, I think we need to make a clear decision ASAP about this because we can't proceed with any of the further pull requests without this...

  3. Right now, yes, mix-and-match representations and differentials are allowed.

  4. That's a fair point!

@eteq
Copy link
Member

eteq commented Jun 9, 2017

@mhvk:
On 1. Fair point - I think I originally did test velocity implementations just as you propose, but eventually we realized attaching them to the representations is important for the reasons @adrn gave above, and because it makes certain aspects of velocity support easier to implement (@adrn and I are working on a couple of PRs for that now). More philosophically, after thinking and talking with a few folks about it, I think this actually does make more sense: basically, you can't do really anything with differentials (other than cartesian) without the representation (and any lower-order derivatives if we implement acceleration et al. in the future), so it makes as much sense to attach them to the representation as shared in the frames.

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 ._data to be a container class (as @adrn said above about 1). And there are people I know of using this representation machinery (most notably Sunpy), which means there are probably more who we don't know about (and won't know about until the start using it and it's all borken). So I think that ship has already sailed.

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 .lat, you shouldn't be forced to transform the differential as it's a performance waste. On some level this is an argument for your point 1, but given the restrictions we described above, it seems like this is the lesser of the various evils.

(I'm ambivalent on 4)

@adrn adrn force-pushed the coordinates/diffs-on-reps branch 2 times, most recently from bc08ff6 to 2725e80 Compare June 13, 2017 16:18
Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

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):
Copy link
Contributor

Choose a reason for hiding this comment

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

@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``
Copy link
Contributor

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

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):
Copy link
Contributor

Choose a reason for hiding this comment

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

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:
Copy link
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Contributor

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

tuple -> dict


r2 = CartesianRepresentation.from_representation(r1)
assert r2.get_name() == 'cartesian'
# assert r2.differentials['s'].get_name() == 'cartesian'
Copy link
Contributor

Choose a reason for hiding this comment

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

delete

def test_to_cartesian():
"""
Test that to_cartesian does the expected thing (converts both the
representation and differentials)
Copy link
Contributor

Choose a reason for hiding this comment

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

Update docstring.

@mhvk
Copy link
Contributor

mhvk commented Jun 18, 2017

OK, pushed my changes, including a second commit with also __repr__ (and a bug fix of the first commit, so be careful if one wants to revert that).

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.

@adrn
Copy link
Member Author

adrn commented Jun 18, 2017

Thanks @mhvk for the careful reading and for submitting changes! From your perspective, is this ready to go if tests all pass? I'd love to have the satisfaction of hitting merge on this one 😄 , but will wait until @eteq chimes in to see if he has any remaining comments.

@mhvk
Copy link
Contributor

mhvk commented Jun 18, 2017

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.

@adrn
Copy link
Member Author

adrn commented Jun 18, 2017

Yes, next is #6218 - talk to you over there!

@adrn
Copy link
Member Author

adrn commented Jun 18, 2017

(Remote data build failures)

Copy link
Member

@eteq eteq left a comment

Choose a reason for hiding this comment

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

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``
Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Member Author

Choose a reason for hiding this comment

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

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,
Copy link
Member

Choose a reason for hiding this comment

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

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):
Copy link
Member

Choose a reason for hiding this comment

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

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):
Copy link
Member

Choose a reason for hiding this comment

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

docstring needs differentials...


def __init__(self, phi, theta, r, copy=True):
def __init__(self, phi, theta, r, differentials=None, copy=True):
super(PhysicsSphericalRepresentation,
Copy link
Member

Choose a reason for hiding this comment

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

docstring needs differentials...


def __init__(self, distance, copy=True):
super(RadialRepresentation, self).__init__(distance, copy=copy)
def __init__(self, distance, differentials=None, copy=True):
Copy link
Member

Choose a reason for hiding this comment

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

docstring needs differentials...


def __init__(self, rho, phi, z, copy=True):
def __init__(self, rho, phi, z, differentials=None, copy=True):
super(CylindricalRepresentation,
Copy link
Member

Choose a reason for hiding this comment

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

docstring needs differentials...


recommended_units = {} # subclasses can override

def __init__(self, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

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()]))
Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Member Author

Choose a reason for hiding this comment

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

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

Copy link
Member Author

Choose a reason for hiding this comment

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

cc @mhvk FYI: this comment is related to why I had removed exposing the units from the __repr__

Copy link
Member

Choose a reason for hiding this comment

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

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.

@eteq
Copy link
Member

eteq commented Jun 18, 2017

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 {u.s: some_diff, u.yr: some_other_diff}. That shouldn't work b/c it's the same physical type so its ambiguous what's going on. @adrn thinks that's already the case, but is going to add a test.

@adrn
Copy link
Member Author

adrn commented Jun 18, 2017

The new test I added passes locally, but since I added quite a few lines of documentation I am trying out the new [docs only] commit message flag to run the documentation builds.

@adrn
Copy link
Member Author

adrn commented Jun 18, 2017

Hm, it seems that the [docs only] flag didn't work, so I manually killed all tests except for the docs build on travis (and circleci since it's usually fast).
cc @bsipocz

@bsipocz
Copy link
Member

bsipocz commented Jun 18, 2017

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

@adrn
Copy link
Member Author

adrn commented Jun 18, 2017

Oh, ignore my comment! I saw the jobs spinning up and jumped to conclusions. I take it back!

@eteq
Copy link
Member

eteq commented Jun 18, 2017

I'm satisfied, so when the doc tests pass feel free to pull the trigger @adrn . You've definitely earned that! 💯

@adrn
Copy link
Member Author

adrn commented Jun 18, 2017

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 build_docs locally to verify those warnings went away.

@adrn adrn merged commit 115e455 into astropy:master Jun 18, 2017
@adrn
Copy link
Member Author

adrn commented Jun 18, 2017

Thanks all for the review -- moving on to #6218

@eteq
Copy link
Member

eteq commented Jun 18, 2017

🚀 🤘 👏

🔁

@adrn adrn deleted the coordinates/diffs-on-reps branch June 18, 2017 19:10
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.

5 participants