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

Adding straw-man API for discussion #4

Merged
merged 5 commits into from
Jun 27, 2015
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions dev/planning-example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# ============================
# Observation Planning Toolbox
# ============================
"""
The observation planning toolbox consists of a number of entities that
simplify the planning and scheduling of observations by abstracting much
of the boilerplate code necessary to accomplish the same thing using
core astropy or ephemerides objects. These can be thought of as common
idioms or convenience classes and functions that would be recreated over
and over by astronomers and or software engineers building telescope
scheduling software.

Important assumptions:

- It should be convenient to specify dates and times in the local timezone
of the observer.

- It should be convenient to do timezone conversions easily.

This means methods that can take dates can either take astropy
dates (as used internally) or python datetime objects (not naive, but
tagged with timezone). So dates can be passed in as timezone-aware
datetime objects and results can be easily converted between such.
Copy link
Member

Choose a reason for hiding this comment

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

Good news on this front: it looks to me like astropy.time.Time does everything exactly like you'd expect with timezones. E.g. Time(datetimeobjwithtzinfo) transparently converts to utc, so all that needs to be done with accepting times is Time(input) and that will work fine if input is a Time or a datetime. And the datetime output of Time is always UTC, so anywhere that you want to get back a local time it can internally just be timeobj.datetime.astimezone(self.timezone)

Copy link
Contributor

Choose a reason for hiding this comment

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

@eteq, do astropy TIme objects respect timezone? In other words, if I pass in a timezone tagged datetime object to initialize a Time object the time will be correctly converted to (my local time) in UTC? In pyephem at least, the timezone was not considered and the time was taken directly to be UTC.

Copy link
Member

Choose a reason for hiding this comment

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

@ejeschke - yep! Note the following:

>>> dt=datetime.datetime.now(pytz.timezone('US/Eastern'))
>>> dt
datetime.datetime(2015, 6, 10, 17, 6, 17, 506659, tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>)
>>> astropy.time.Time(dt)
<Time object: scale='utc' format='datetime' value=2015-06-10 21:06:17.506659>

As you can see, the final Time object has the correct UTC time even though I gave it something that is in local time.


"""

# ================
# Observer objects
# ================
"""
An observer defines a single terrestrial location and applicable
environmental attributes thereof that affect observation calculations
at that location.
"""
# Define an observer by full specification.
# Latitude/Longitude take input as strings or quantities,
# elevation, pressure, temperature must be quantities.
# Accept `timezone` from pytz
from astroplan import Observer
import astropy.units as u
from astropy.coordinates import EarthLocation
import pytz

# Define the observer with an instance of astropy.coordinates.EarthLocation,
# also use pytz.timezone() argument directly as `timezone` keyword's input
longitude = '-155d28m48.900s'
latitude = '+19d49m42.600s'
elevation = 4163 * u.m
location = EarthLocation.from_geodetic(longitude, latitude, elevation)

obs = Observer(name='Subaru Telescope',
Copy link
Member

Choose a reason for hiding this comment

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

Curious about the choice of Observer vs. Observatory

Copy link
Contributor

Choose a reason for hiding this comment

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

It's basically defining a terrestrial location along with some environmental data necessary to make calculations. Could also use the name Site or Location, etc. I just prefer Observer.

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 Observer or Site (or ObservingSite?) is fine - Location is to be avoided as it might get confused with astropy.coordinates.EarthLocation.

I'd say reserve Observatory for a possible future subclass which might include extra information like telescopes, instrument configurations, etc.

location=location,
pressure=0.615 * u.bar,
relative_humidity=0.11,
temperature=0 * u.deg_C,
timezone=pytz.timezone('US/Hawaii'),
description="Subaru Telescope on Mauna Kea, Hawaii")

# It would also be desirable to be able to have a small database of
# common telescopes. Maybe this can at first simply take the form of
# a python module:
from astroplan import sites

obs = sites.Keck1

# Environmental conditions should be updatable.
obs.pressure = 0.600 * u.bar
obs.relative_humidity = 0.2
obs.temperature = 10 * u.deg_C

# Dates are assumed to be UTC by default

"""
We can ask about typical times of interest at this observing position
Returned dates are assumed to be in the observer's time zone for convenience.
The date parameter, if given, can be an astropy Time or a datetime instance.
If no parameter is given, the current date/time is assumed.
Copy link
Member

Choose a reason for hiding this comment

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

A subtle question about date: it's not obvious what "date" that's referring to. That is, are we asking about the dark period that's the morning of that day, or the night of that day?

I moderately common (but not universal) convention in astronomy is to say a date and implictly take that to mean "the night of that date". If we take that convention, then instead of date the keyword should be nightof or similar. However, that has a problem: what if I'm in pacific time and specify the date as '2015-05-01 01:00:00 Pacific'? A human reading that (or an equivalent datetime or Time object) would probably assume that means the morning of May 1/night of April 30, but the nightof convention would take it to be the night of May 1/May 2.

So an alternative is to have it be "the dark period closest to that date". That would require some calculation, though, so it has a performance overhead. One way to mitigate that might be to special-case "pure dates", like the string '2015-05-01' or a datetime.date object.

Another alternative is to only allow pure dates, which could go with the "night of" convention.

There might be other options, but those are the ones that come to mind for me.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like the idea of nightof--it is clearer.

"""

# Define a time
from astropy.time import Time
time_obs = Time(2457189.500000, format='jd')

# sunset
obs.sunset(time=time_obs, which='nearest') # Default
obs.sunset(time=time_obs, which='next')
obs.sunset(time=time_obs, which='previous')
obs.sunset() # Will be use time=Time.now(), which='nearest'

# sunrise
obs.sunrise(time=time_obs, which='nearest') # Default
obs.sunrise(time=time_obs, which='next')
obs.sunrise(time=time_obs, which='previous')

# moon rise
obs.moonrise(time=time_obs, which='nearest')

# moon set
obs.moonset(time=time_obs, which='nearest')

# The above functions can be called with an `angle` keyword argument to specify
# a particular horizon angle for rising or setting, or can be called with
# convenience functions for particular morning/evening twilight.
# For example, to compute astronomical twilight by specifying the `angle`:
obs.sunset(time=time_obs, which='next', angle=18*u.degree)

# evening (astronomical) twilight
obs.evening_astronomical(time=time_obs)

# evening (nautical) twilight
obs.evening_nautical(time=time_obs)

# evening (civil) twilight
obs.evening_civil(time=time_obs)

Copy link
Member

Choose a reason for hiding this comment

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

Two other useful sun-related methods that are missing: obs.noon(date=day) and obs.midnight(date=day).

# morning (nautical) twilight
obs.morning_nautical(time=time_obs)

# morning (civil) twilight
obs.morning_civil(time=time_obs)

# morning (astronomical) twilight
obs.morning_astronomical(time=time_obs)

# what is the moon illumination?
# returns a float, which is percentage of the moon illuminated
obs.moon_illumination(time=time_obs)

# what is the moon altitude and azimuth?
obs.moon_altaz(time=time_obs)

# Other sun-related convenience functions:
obs.noon(time=time_obs, which='nearest')
obs.midnight(time=time_obs, which='nearest')

Copy link
Member

Choose a reason for hiding this comment

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

Also possibly useful: moon_rise and moon_set.

# ==============
# Target objects
# ==============
'''
A target object defines a single observation target, with coordinates
and an optional name.
'''
from astroplan import FixedTarget

# Define a target.

from astropy.coordinates import SkyCoord
t1 = FixedTarget(name='Polaris',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@eteq @cdeil When I make FixedTarget by subclassing SkyCoord, the attribute name is already assigned (and it looks like it returns the string for the name of the frame). Should we consider a different keyword for the name, like object (probably not the best choice)?

Copy link
Member

Choose a reason for hiding this comment

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

Why sub-class? Has this been discussed before?

"A target has a sky coordinate" suggests that composition (a SkyCoord member) could be the right object-oriented design.
Usually sub-classing is used for is a relationships, but "A target is a sky coordinate" isn't quite right, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's been mentioned as an option, but not thoroughly discussed. It makes sense to (naive) me because the most fundamental operations we'll do on FixedTargets are operations on the coordinates.

I'm imaging the FixedTarget as a thin subclass with extra keywords, like this:

class FixedTarget(SkyCoord):
    def __init__(self, *args, **kwargs):
        # Pull out FixedTarget specific keywords before passing onto SkyCoord
        self.object = kwargs.pop('object', None)
        self.metadata = kwargs.pop('metadata', {})

        # Pass remaining keywords on to initialize SkyCoord
        SkyCoord.__init__(self, *args, **kwargs)

If the Fixedtarget class is a top level class, I think almost every operation we'd code would have to be on a .coord attribute, which seems less clean to me.

Copy link
Member

Choose a reason for hiding this comment

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

@bmorris3 – OK, I can see how for FixedTargetit's appealing to subclass SkyCoord to avoid repetitive target.coord.. On the other hand, if target.name is not the target name, that's also not nice. A mixed option might be to have a target.coord member, but then re-expose most or some of it's attributes on the target class.

Basically I'm just guessing here what a good design could be ... @eteq, @adrn, @ejeschke ?

Copy link
Member

Choose a reason for hiding this comment

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

I would recommend not trying to subclass SkyCoord -- there is a lot of magic that is probably better not understood for the time being...

I also don't think we need to expose coord-level methods/attributes to Target -- aren't namespaces good?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

More fuel for the conversation – right now the FixedTarget class doesn't do very much more than the equivalent SkyCoord other than holding a name, and potentially in the future holding mags and epochs/periods. Will FixedTarget require enough supporting code to necessitate its own class?

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 I agree that this should not subclass from SkyCoord - as @cdeil said it has a coordinate, rather than is one.

Put another way, we say "The thing I want to target is Polaris. It is a star with the name Polaris, and it is located at 2h31m/89d15m." One generally does not say "I am targeting a location on the sky that has the name Polaris".

More broadly, we expect there will be other kind of targets down the road that will not be subclasses of SkyCoord (i.e.targets that follow the path of the moon and hence their SkyCoord changes a lot over the course of the night). So it might be confusing for some to have coordinates and others to be coordinates.

So I'd say @bmorris3 is right that in most cases just storing a name doesn't justify a class, but here we have plans for future classes that justify it better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I think we're settled on this. Especially now that I see that @jberlanga had implemented FixedTarget as a subclass of a Target super class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Chiming in late here, but completely in favor of making the skycoord an attribute of the target class, rather than a subclass.

coord=SkyCoord('02h31m49.09s', '+89d15m50.8s', frame='icrs'))

Copy link
Member

Choose a reason for hiding this comment

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

Might be handy to have an airmass_plot(date=xx) convenience method so you can do, e.g.:

fig = t1.airmass_plot(observatory=obs, datetime=times)
fig = t2.airmass_plot(observatory=obs, datetime=times, fig=fig)
plt.show()

and quickly see airmass curves for objects. Not sure...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm wondering if we should leave more provisions to prepare the SiderealTarget for interfacing with scheduling code. Would we create kwargs for SiderealTarget that can store magnitudes in various bandpasses or ephemerides (phase and epoch)? To return to your point on the Hangout @adrn , exoplanet observers like me and others will have other fundamental quantities that are as important to the targets' observability as their coords.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds like we should make provisions for adding appropriate metadata to targets. I would note that the Condition objects (below) could easily have tests for magnitude ranges, etc.

Copy link
Member

Choose a reason for hiding this comment

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

Note that you can always just add a name or meta attribute to a SkyCoord and it will work just fine.

Re: @adrn's original point, I think airmass_plot is better implemented as a separate function or possibly even a class, rather than as a method. Or it could be both (a la the matching functions in astropy.coordinates and the corresponding SkyCoord methods), although I prefer to avoid that unless there's a specific need (for coordinates it's that SkyCoord and lower-level objects both can be used with the functions).

# Leaves scope for NonSiderealTarget, etc.

# Convenience methods for looking up/constructing targets by name via
# astroquery:
t1 = FixedTarget.from_name('Polaris')

# ================================
# Condition objects, observability
# ================================

"""
Q: Can I observe this target on the night of May 1, 2015 between 18:30
and 05:30 HST, above airmass 1.2, on a telescope that can observe between
Copy link
Member

Choose a reason for hiding this comment

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

Please bear with me ... I'm a gamma-ray astronomer that never used airmass. I know what altitude and zenith angle is.

What does "above airmass 1.2" mean?
I presume usually the constraint people want is to observe at low zenith angles = high altitudes, right?
And that corresponds to low airmass, so usually people want an upper limit as an airmass constraint, no?
https://en.wikipedia.org/wiki/Air_mass_(astronomy)#/media/File:AirmassFormulaePlots.png

But in this API spec and at http://astroplan.readthedocs.org/en/latest/api/astroplan.AboveAirmass.html we only allow to specify a lower limit.

Here's what I think:

  • The relation of airmass and altitude should be explained in the airmass constraint docstring
  • The user should be able to constrain airmass low / high, just like it's possible for altitude. The default could be None for both, meaning no constraint on either min or max?
  • I would prefer something regular for min / max constraints ... use TimeRange, AltitudeRange and AirmassRange. (at the moment we have AboveAirmass, AltitudeRange, TimeWindow.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, bad wording on my part. By "above airmass 1.2" I meant "at a better airmass" (conflating with elevation in my attempt to be concise). +1 for TimeRange, AltitudeRange, and AirmassRange.

Copy link
Member

Choose a reason for hiding this comment

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

To close the loop on this, a recent change made this AirmassRange, but it can still be used in the single-argument form to mean "at this airmass or better".

15 degrees and 89 degrees altitude?
"""

# first we define the conditions
from astroplan import TimeRange, AltitudeRange, AirmassRange, is_observable
# `is_observable` is a temporary function which will eventually be a method of
# something to support caching

# Times in TimeRange can be passed in as strings, will be passed to the
# Time constructor.
constraint_list = [TimeRange("2015-05-01 18:30", "2015-05-02 05:30"),
AirmassRange(1.2), AltitudeRange(15.0*u.deg, 89.0*u.deg)]

# (AboveAirmass will be a subclass of AltitudeWindow)

# Combine a list of constraints to run on Observer, FixedTarget, and time to
# determine the observability of target
constraints = is_observable(constraint_list, obs, t1, time_obs)
Copy link
Member

Choose a reason for hiding this comment

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

Can you leave a comment here that is_observable is a "provisional" name and it should probably end up a method of something? I'm fine with the choice of kicking the decision down the road. But I want to make sure we don't end up with an API that makes it harder to work fast (which could happen with this as a function instead of a method because it doesn't have a place to cache anything.)


# Test only a single constraint:
constraints = is_observable(AirmassRange(1.2), obs, t1, time_obs)

# AirmassRange can accept two bounding airmasses, assumes single argument is
# an upper limit, lower limit = 1.

# `constraints` will be a boolean where True=observable. For a list of
# targets, observatories, or times, `constraints` may be a booleans array

# We will eventually need a more complicated method that minimizes a cost
# function when optimizing an observing schedule given the results of
# `is_observable`.

# ======================================================
# Other useful calculations wrt an observer and a target
#=======================================================

# calculate the distance in alt and az degrees between two targets at
# the given time (e.g. to calculate slew time)
sf = FixedTarget(name='Sf', coord=SkyCoord('09d40m00.00s', '43d00m00.00s'))
sm = FixedTarget(name='Sm', coord=SkyCoord('10d30m00.00s', '36d00m00.00s'))

# Coordinate arithmetic gives separations in RA, Dec, alt, az
dra, ddec = sf.ra - sm.ra, sf.dec - sm.dec
dalt = obs.altaz(sf, time_obs).alt - obs.altaz(sm, time_obs).alt
dazt = obs.altaz(sf, time_obs).az - obs.altaz(sm, time_obs).az