-
-
Notifications
You must be signed in to change notification settings - Fork 111
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
|
||
""" | ||
|
||
# ================ | ||
# 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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious about the choice of There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd say reserve |
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A subtle question about 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 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of |
||
""" | ||
|
||
# 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) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two other useful sun-related methods that are missing: |
||
# 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') | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also possibly useful: |
||
# ============== | ||
# 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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bmorris3 – OK, I can see how for Basically I'm just guessing here what a good design could be ... @eteq, @adrn, @ejeschke ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would recommend not trying to subclass I also don't think we need to expose coord-level methods/attributes to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More fuel for the conversation – right now the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I agree that this should not subclass from 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 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be handy to have an
and quickly see airmass curves for objects. Not sure... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we should leave more provisions to prepare the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that you can always just add a Re: @adrn's original point, I think |
||
# 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? 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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To close the loop on this, a recent change made this |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you leave a comment here that |
||
|
||
# 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 |
There was a problem hiding this comment.
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 isTime(input)
and that will work fine ifinput
is aTime
or adatetime
. And thedatetime
output ofTime
is always UTC, so anywhere that you want to get back a local time it can internally just betimeobj.datetime.astimezone(self.timezone)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
As you can see, the final
Time
object has the correct UTC time even though I gave it something that is in local time.