Skip to content
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

Coordinate frames that include solar differential rotation #3537

Merged
merged 11 commits into from
Feb 5, 2020

Conversation

ayshih
Copy link
Member

@ayshih ayshih commented Nov 22, 2019

This PR integrates solar (differential) rotation into the coordinates framework. The approach here is to enable defining a frame that is a modified version of a base coordinate frame by a duration of solar (differential) rotation. The class factory RotatedSunFrame is supplied the "base" coordinate frame, and creates a base-specific rotated-Sun frame class.

Closes #3357

Still needs:

Walkthrough

>>> import astropy.units as u
>>> from sunpy.coordinates import RotatedSunFrame
>>> import sunpy.coordinates.frames as f

Propagating a coordinate

Let's define a coordinate array of points in a rotated-Sun frame that in the base HGS frame were on the meridian as seen from Earth. Here, the rotated-Sun frame applies +1 day of solar rotation.

>>> hgs = f.HeliographicStonyhurst(obstime='2001-01-01')
>>> rotated_arc = RotatedSunFrame([0]*11*u.deg, range(-75, 90, 15)*u.deg, base=hgs, duration=1*u.day)
>>> rotated_arc
<RotatedSunHeliographicStonyhurst Coordinate (base=<HeliographicStonyhurst Frame (obstime=2001-01-01T00:00:00.000)>, duration=1.0 d, rotation_model=howard): (lon, lat, radius) in (deg, deg, km)
    [(0., -75., 695700.), (0., -60., 695700.), (0., -45., 695700.),
     (0., -30., 695700.), (0., -15., 695700.), (0.,   0., 695700.),
     (0.,  15., 695700.), (0.,  30., 695700.), (0.,  45., 695700.),
     (0.,  60., 695700.), (0.,  75., 695700.)]>

The coordinate components are relative to the distorted axes of the rotated-Sun frame, and thus are already differentially rotated in real space. Convert the coordinate array back to the HGS frame to see what I mean.

>>> rotated_arc.transform_to(hgs)
<HeliographicStonyhurst Coordinate (obstime=2001-01-01T00:00:00.000): (lon, lat, radius) in (deg, deg, km)
    [(10.7550473 , -75., 695700.), (11.70697161, -60., 695700.),
     (12.80904447, -45., 695700.), (13.68216339, -30., 695700.),
     (14.17617983, -15., 695700.), (14.32632838,   0., 695700.),
     (14.17617983,  15., 695700.), (13.68216339,  30., 695700.),
     (12.80904447,  45., 695700.), (11.70697161,  60., 695700.),
     (10.7550473 ,  75., 695700.)]>

Another example

There are several ways to instantiate a RotatedSunFrame frame. Here, the input supplied to base has the relevant coordinate data, so that data is automatically "moved" to the correct place in the resulting frame. Also, instead of supplying duration directly, the rotated_time keyword is used to indirectly define the duration of rotation.

>>> center = f.Helioprojective(0*u.arcsec, 0*u.arcsec, obstime='2001-01-01', observer='earth')
>>> rotated_center = RotatedSunFrame(base=center, rotated_time='2001-01-03')
>>> rotated_center
<RotatedSunHelioprojective Coordinate (base=<Helioprojective Frame (obstime=2001-01-01T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate for 'earth'>)>, duration=2.0 d, rotation_model=howard): (Tx, Ty) in arcsec
    (0., 0.)>

No surprises here: The HPC center of the solar disk moves mostly in the X direction after +2 days of solar rotation.

>>> rotated_center.transform_to(center)
<Helioprojective Coordinate (obstime=2001-01-01T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate for 'earth'>): (Tx, Ty, distance) in (arcsec, arcsec, AU)
    (468.86076705, -6.35238293, 0.97923501)>

Because RotatedSunFrame frames are in the transformation graph, you can do more creative transformations. Here, we calculate what HPC coordinate when differentially rotated by -1 days corresponds to the disk center when differentially rotated by +2 days.

>>> rotated_center.transform_to(RotatedSunFrame(base=center, duration=-1*u.day))
<RotatedSunHelioprojective Coordinate (base=<Helioprojective Frame (obstime=2001-01-01T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate for 'earth'>)>, duration=-1.0 d, rotation_model=howard): (Tx, Ty, distance) in (arcsec, arcsec, AU)
    (666.1751848, -13.91351551, 0.97991384)>

Grid overlay on a Map

>>> from sunpy.map import Map
>>> from sunpy.data.sample import AIA_171_IMAGE
>>> m = Map(AIA_171_IMAGE)

>>> import matplotlib.pyplot as plt
>>> ax = plt.subplot(projection=m)
>>> m.plot()

>>> overlay = ax.get_coords_overlay('heliographic_stonyhurst')
>>> overlay[0].set_ticks(spacing=15. * u.deg)
>>> overlay[1].set_ticks(spacing=90. * u.deg)
>>> overlay.grid(ls='-', color='white')

>>> overlay = ax.get_coords_overlay(RotatedSunFrame(base=f.HeliographicStonyhurst(obstime=m.date), duration=27*u.day))
>>> overlay[0].set_ticks(spacing=15. * u.deg)
>>> overlay[1].set_ticks(spacing=90. * u.deg)
>>> overlay.grid(ls='-', color='blue')

diffrot

@pep8speaks
Copy link

pep8speaks commented Nov 22, 2019

Hello @ayshih! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

Line 25:9: E128 continuation line under-indented for visual indent

Line 102:20: E721 do not compare types, use 'isinstance()'
Line 163:20: E721 do not compare types, use 'isinstance()'

Comment last updated at 2020-02-05 15:01:21 UTC

@ghost
Copy link

ghost commented Nov 22, 2019

Thanks for the pull request @ayshih! Everything looks great!

@ayshih ayshih requested a review from Cadair November 22, 2019 19:08
@ayshih ayshih added the coordinates Affects the coordinates submodule label Nov 22, 2019
@ayshih ayshih force-pushed the diffrotdynamic branch 3 times, most recently from dc01ef7 to f7bd66e Compare November 22, 2019 21:18
@ayshih
Copy link
Member Author

ayshih commented Nov 22, 2019

Multiple rotation durations

You can work with array obstime/duration/rotated_time.

>>> hpc = Helioprojective(obstime='2001-01-01', observer='earth')
>>> sc = SkyCoord([0]*9*u.arcsec, [0]*9*u.arcsec, frame=hpc)
>>> series = RotatedSunFrame(base=sc, duration=range(0, 27, 3)*u.day)
>>> series
<RotatedSunHelioprojective Coordinate (base=<Helioprojective Frame (obstime=2001-01-01T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate for 'earth'>)>, duration=[ 0.  3.  6.  9. 12. 15. 18. 21. 24.] d, rotation_model=howard): (Tx, Ty) in arcsec
    [(0., 0.), (0., 0.), (0., 0.), (0., 0.), (0., 0.), (0., 0.), (0., 0.),
     (0., 0.), (0., 0.)]>
>>> series.transform(hpc)
<Helioprojective Coordinate (obstime=2001-01-01T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate for 'earth'>): (Tx, Ty, distance) in (arcsec, arcsec, AU)
    [(   0.        ,    0.        , 0.97866506),
     ( 666.1751848 ,  -13.91351551, 0.97991384),
     ( 971.99406549,  -48.04083807, 0.98298359),
     ( 756.05760587,  -83.90802534, 0.98622017),
     ( 137.55563461, -102.40288732, 0.98789324),
     (-553.88198072,  -93.79075537, 0.98711382),
     (-951.06271076,  -62.59442403, 0.9842956 ),
     (-839.39922086,  -25.34560595, 0.98094109),
     (-274.84399172,   -2.09057135, 0.9788526 )]>

@ayshih
Copy link
Member Author

ayshih commented Nov 23, 2019

Transforming a coordinate to a frame with a different observation time

In the coordinates framework, a coordinate (e.g., SkyCoord) points to a specific location in inertial space, while the obstime/observer define the origin and axis orientations of the coordinate frame. If you transform a SkyCoord to a coordinate frame with a different obstime, you obtain the same inertial location in the new coordinate frame, because there there isn't any way to specify how that coordinate evolves in time.

For example, here we try to transform disk center as seen by one observer to an observer 90 degrees separated in longitude and 6 days later in time:

>>> observer1 = SkyCoord(HeliocentricInertial(0*u.deg, 0*u.deg, 1*u.AU, obstime='2001-01-01'))
>>> observer2 = SkyCoord(HeliocentricInertial(90*u.deg, 0*u.deg, 1*u.AU, obstime='2001-01-07'))

>>> frame1 = Helioprojective(observer=observer1, obstime=observer1.obstime)
>>> frame2 = Helioprojective(observer=observer2, obstime=observer2.obstime)

>>> sc = SkyCoord(0*u.arcsec, 0*u.arcsec, frame=frame1)
>>> sc.transform_to(frame2)
<SkyCoord (Helioprojective: obstime=2001-01-07T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate (obstime=2001-01-07T00:00:00.000): (lon, lat, radius) in (deg, deg, AU)
    (59.22396701, 0., 1.)>): (Tx, Ty, distance) in (arcsec, arcsec, AU)
    (-956.93749939, 1.16033026, 1.00006359)>

The output coordinate is on the east limb, which is consistent with not evolving the location with solar rotation.

With this PR, you can now include the evolution of solar rotation via this call:

>>> RotatedSunFrame(base=sc, rotated_time=frame2.obstime).transform_to(frame2)
<Helioprojective Coordinate (obstime=2001-01-07T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate (obstime=2001-01-07T00:00:00.000): (lon, lat, radius) in (deg, deg, AU)
    (59.22396701, 0., 1.)>): (Tx, Ty, distance) in (arcsec, arcsec, AU)
    (-78.69026983, 1.16579504, 0.99537569)>

The RotatedSunFrame applies the differential rotation of the 6 days, and then the coordinate is transformed to the new coordinate frame.

@Cadair Cadair added this to the 2.0 milestone Nov 23, 2019
sunpy/coordinates/metaframes.py Outdated Show resolved Hide resolved
sunpy/coordinates/metaframes.py Outdated Show resolved Hide resolved
sunpy/coordinates/metaframes.py Show resolved Hide resolved
sunpy/coordinates/metaframes.py Outdated Show resolved Hide resolved
@ayshih
Copy link
Member Author

ayshih commented Nov 27, 2019

Differentially rotating a map (versus the old approach)

We can use this frame, plus reproject, to mimic the old approach to produce a differentially rotated map (differential_rotate). This requires reproject 0.6 as well as PR #3540. Importantly, the target frame for the reprojection needs to be the output frame differentially rotated to the time of the input frame.

comparison

import matplotlib.pyplot as plt

from reproject import reproject_interp

from astropy.coordinates import SkyCoord
import astropy.units as u
import sunpy.map
from astropy.wcs import WCS
from sunpy.coordinates import RotatedSunFrame, get_body_heliographic_stonyhurst, frames as f
from sunpy.data.sample import AIA_171_IMAGE
from sunpy.physics.differential_rotation import differential_rotate

delta_t = 5*u.day

m = sunpy.map.Map(AIA_171_IMAGE)

# Define the output frame with an associated new observer (at Earth)
output_frame = f.Helioprojective(observer='earth', obstime=m.date + delta_t)

# Define the RotatedSunFrame (its `duration` will turn out to be -1 * `delta_t`)
rot_frame = RotatedSunFrame(base=output_frame, rotated_time=m.date)

out_shape = (1024, 1024)
header = sunpy.map.make_fitswcs_header(out_shape,
                                       m.center.transform_to(rot_frame).as_base(),
                                       scale=u.Quantity(m.scale))

out_wcs = WCS(header)
out_wcs.coordinate_frame = rot_frame

with sunpy.coordinates.transformations.ignore_sun_motion():
    arr, foot = reproject_interp(m, out_wcs, out_shape)

out_warp = sunpy.map.Map(arr, out_wcs)
out_warp.plot_settings = m.plot_settings

old = differential_rotate(m, time=delta_t)

fig = plt.figure(figsize=(15,8))
ax = plt.subplot(1,2,1, projection=out_warp)
out_warp.plot(ax)
ax.set_title("Reproject using RotatedSunFrame")
overlay = ax.get_coords_overlay(
    RotatedSunFrame(base=f.HeliographicStonyhurst(obstime=m.date), rotated_time=output_frame.obstime))
overlay[0].set_ticks(spacing=15. * u.deg)
overlay[1].set_ticks(spacing=90. * u.deg)
with sunpy.coordinates.transformations.ignore_sun_motion():
    overlay.grid(ls='-', color='blue')

ax2 = plt.subplot(1,2,2,projection=old)
old.plot(ax2)
ax2.set_title("Using differential_rotate")

plt.show()

@ayshih
Copy link
Member Author

ayshih commented Dec 18, 2019

Now rebased off of #3540 so that I can use the context manager in examples. Presumably #3540 will be merged before this PR.

@ayshih ayshih force-pushed the diffrotdynamic branch 3 times, most recently from 81edbcf to e57f274 Compare December 18, 2019 18:58
@ayshih
Copy link
Member Author

ayshih commented Dec 18, 2019

I've added a tutorial (see this recent build).

@Cadair
Copy link
Member

Cadair commented Dec 19, 2019

The tutorial is awesome! Are we confident in the reproject example doing the right thing?

@Cadair
Copy link
Member

Cadair commented Dec 19, 2019

We should also open an issue on astropy about specifying the frame for a WCS. I spoke to both Tom and Erik about it at the astropy meeting and they both agreed it would be something useful to workout upstream.

@ayshih
Copy link
Member Author

ayshih commented Dec 19, 2019

The tutorial is awesome!

Thanks! I just added another example to show using plot_coord (see recent build).

Are we confident in the reproject example doing the right thing?

Well, I'm confident that the coordinate transformations are correct. Are you confident that reproject is doing the right thing? =P Seriously though, the lingering discrepancies between the "new" method and the "old" method may be difficult to reconcile since we don't actually know that the "old" method was accurate.

@ayshih ayshih force-pushed the diffrotdynamic branch 4 times, most recently from b0257a2 to c0e70d2 Compare December 20, 2019 02:39
@ayshih
Copy link
Member Author

ayshih commented Jan 17, 2020

Okay, I've committed the test. Who's brave enough to review this? =P

@ayshih ayshih force-pushed the diffrotdynamic branch 2 times, most recently from c35843a to 18fa69f Compare January 17, 2020 16:35
@Cadair
Copy link
Member

Cadair commented Jan 17, 2020

Well it just took me 20 mins to read through your docs so...

Copy link
Member

@Cadair Cadair left a comment

Choose a reason for hiding this comment

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

This is amazing! So cool! I particularly love the wcsaxes example, but I think it is exceptionally well documented and tested. Thanks @ayshih 🚀

Super minimal changes below, but looks good to me. Now, are we going to find a second reviewer for this?!

sunpy/coordinates/metaframes.py Show resolved Hide resolved
sunpy/coordinates/metaframes.py Outdated Show resolved Hide resolved
sunpy/coordinates/metaframes.py Outdated Show resolved Hide resolved
sunpy/coordinates/metaframes.py Outdated Show resolved Hide resolved
@@ -15,6 +15,9 @@ def solar_wcs_frame_mapping(wcs):
type values in the `astropy.wcs.utils.wcs_to_celestial_frame` registry.
"""

if hasattr(wcs, "coordinate_frame"):
Copy link
Member

Choose a reason for hiding this comment

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

We really need to upstream this into Astropy...

sunpy/coordinates/tests/test_metaframes.py Outdated Show resolved Hide resolved
@Cadair
Copy link
Member

Cadair commented Jan 17, 2020

Now needs a rebase because #3540 has been merged.

Copy link
Member

@dstansby dstansby left a comment

Choose a reason for hiding this comment

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

I haven't thoroughly reviewed the code, but have left some documentation suggestions (and one question).

sunpy/coordinates/frames.py Show resolved Hide resolved
@@ -0,0 +1,390 @@
.. _sunpy-coordinates-rotatedsunframe:

Differential rotation using coordinate frames
Copy link
Member

@dstansby dstansby Feb 5, 2020

Choose a reason for hiding this comment

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

I think in this section there should be a clear reference to the available methods (ie. formulae) that can be used for the rotation.


Creating coordinates
^^^^^^^^^^^^^^^^^^^^

Copy link
Member

Choose a reason for hiding this comment

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

There are lots of class links here that should be intersphinx links and not double-dashes. Happy for another issue to be opened to fix this though.

<HeliographicCarrington Coordinate (obstime=2020-03-04T00:00:00.000): (lon, lat, radius) in (deg, deg, km)
(10., 20., 695700.)>

Advanced uses of RotatedSunFrame
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 examples would be better in individual gallery examples, since this is already quite an individually long bit of documentation.

Copy link
Member

@dstansby dstansby left a comment

Choose a reason for hiding this comment

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

Unless @ayshih wants to put my doc suggestions in here, I'm happy for them to be farmed out to another issue and then another PR.

@Cadair Cadair merged commit 3c89110 into sunpy:master Feb 5, 2020
@Cadair
Copy link
Member

Cadair commented Feb 5, 2020

@dstansby or @ayshih can you open an issue to track the documentation improvements please?

@dstansby
Copy link
Member

dstansby commented Feb 5, 2020

👍 will do

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
coordinates Affects the coordinates submodule
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement differential rotation on the coordinate transform graph
4 participants