diff --git a/astroplan/constraints.py b/astroplan/constraints.py index f9857fe9..630a1252 100644 --- a/astroplan/constraints.py +++ b/astroplan/constraints.py @@ -16,7 +16,7 @@ # Third-party from astropy.time import Time import astropy.units as u -from astropy.coordinates import get_body, get_sun, Galactic, SkyCoord +from astropy.coordinates import Galactic, SkyCoord from astropy import table import numpy as np @@ -27,6 +27,8 @@ from .utils import time_grid_from_range from .target import get_skycoord from .exceptions import MissingConstraintWarning +from .observer import _make_cache_key + __all__ = ["AltitudeConstraint", "AirmassConstraint", "AtNightConstraint", "is_observable", "is_always_observable", "time_grid_from_range", @@ -44,47 +46,6 @@ ) -def _make_cache_key(times, targets): - """ - Make a unique key to reference this combination of ``times`` and ``targets``. - - Often, we wish to store expensive calculations for a combination of - ``targets`` and ``times`` in a cache on an ``observer``` object. This - routine will provide an appropriate, hashable, key to store these - calculations in a dictionary. - - Parameters - ---------- - times : `~astropy.time.Time` - Array of times on which to test the constraint. - targets : `~astropy.coordinates.SkyCoord` - Target or list of targets. - - Returns - ------- - cache_key : tuple - A hashable tuple for use as a cache key - """ - # make a tuple from times - try: - timekey = tuple(times.jd) + times.shape - except BaseException: # must be scalar - timekey = (times.jd,) - # make hashable thing from targets coords - try: - if hasattr(targets, 'frame'): - # treat as a SkyCoord object. Accessing the longitude - # attribute of the frame data should be unique and is - # quicker than accessing the ra attribute. - targkey = tuple(targets.frame.data.lon.value.ravel()) + targets.shape - else: - # assume targets is a string. - targkey = (targets,) - except BaseException: - targkey = (targets.frame.data.lon,) - return timekey + targkey - - def _get_altaz(times, observer, targets, force_zero_pressure=False): """ Calculate alt/az for ``target`` at times linearly spaced between @@ -471,7 +432,7 @@ def _get_solar_altitudes(self, times, observer, targets): observer.pressure = 0 # find solar altitude at these times - altaz = observer.altaz(times, get_sun(times)) + altaz = observer.altaz(times, observer.get_body("sun", times)) altitude = altaz.alt # cache the altitude observer._altaz_cache[aakey] = dict(times=times, @@ -549,7 +510,7 @@ def compute_constraint(self, times, observer, targets): # centred frame, so the separation is as-seen # by the observer. # 'get_sun' returns ICRS coords. - sun = get_body('sun', times, location=observer.location) + sun = observer.get_body('sun', times) targets = get_skycoord(targets) solar_separation = sun.separation(targets) @@ -591,7 +552,7 @@ def __init__(self, min=None, max=None, ephemeris=None): self.ephemeris = ephemeris def compute_constraint(self, times, observer, targets): - moon = get_body("moon", times, location=observer.location, ephemeris=self.ephemeris) + moon = observer.get_body("moon", times, ephemeris=self.ephemeris) # note to future editors - the order matters here # moon.separation(targets) is NOT the same as targets.separation(moon) # the former calculates the separation in the frame of the moon coord diff --git a/astroplan/observer.py b/astroplan/observer.py index 03f148dc..0c2fa172 100644 --- a/astroplan/observer.py +++ b/astroplan/observer.py @@ -8,7 +8,7 @@ import datetime import warnings # Third-party -from astropy.coordinates import (EarthLocation, SkyCoord, AltAz, get_sun, +from astropy.coordinates import (EarthLocation, SkyCoord, AltAz, get_body, Angle, Longitude) import astropy.units as u from astropy.time import Time @@ -27,6 +27,47 @@ MAGIC_TIME = Time(-999, format='jd') +def _make_cache_key(times, targets): + """ + Make a unique key to reference this combination of ``times`` and ``targets``. + + Often, we wish to store expensive calculations for a combination of + ``targets`` and ``times`` in a cache on an ``observer``` object. This + routine will provide an appropriate, hashable, key to store these + calculations in a dictionary. + + Parameters + ---------- + times : `~astropy.time.Time` + Array of times on which to test the constraint. + targets : `~astropy.coordinates.SkyCoord` + Target or list of targets. + + Returns + ------- + cache_key : tuple + A hashable tuple for use as a cache key + """ + # make a tuple from times + try: + timekey = tuple(times.ravel().jd) + times.shape + except BaseException: # must be scalar + timekey = (times.jd,) + # make hashable thing from targets coords + try: + if hasattr(targets, 'frame'): + # treat as a SkyCoord object. Accessing the longitude + # attribute of the frame data should be unique and is + # quicker than accessing the ra attribute. + targkey = tuple(targets.frame.data.lon.value.ravel()) + targets.shape + else: + # assume targets is a string. + targkey = (targets,) + except BaseException: + targkey = (targets.frame.data.lon,) + return timekey + targkey + + # Handle deprecated MAGIC_TIME variable def deprecation_wrap_module(mod, deprecated): """Return a wrapped object that warns about deprecated accesses""" @@ -531,6 +572,58 @@ def _preprocess_inputs(self, time, target=None, grid_times_targets=False): .format(time.shape, target.shape)) return time, target + def get_body(self, body, time, ephemeris=None): + """ + Get a solar system body from `~astropy.coordinates.get_body` for the + current observer location. Results are cached to speed up multiple + calls for the same body and time. + + Parameters + ---------- + body : str + Name of solar system body. + + time : `~astropy.time.Time` or other (see below) + The time at which the observation is taking place. Will be used as + the ``obstime`` attribute in the resulting frame or coordinate. This + will be passed in as the first argument to the `~astropy.time.Time` + initializer, so it can be anything that `~astropy.time.Time` will + accept (including a `~astropy.time.Time` object) + + ephemeris : str, optional + Ephemeris to use. If not given, use the one set with + ``astropy.coordinates.solar_system_ephemeris.set`` (which is + set to 'builtin' by default). + + Returns + ------- + `~astropy.coordinates.SkyCoord` + The position of the solar system body at the requested time. + + Examples + -------- + >>> from astroplan import Observer + >>> from astropy.time import Time + >>> from astropy.coordinates import SkyCoord + >>> apo = Observer.at_site("APO") + >>> time = Time('2001-02-03 04:05:06') + >>> target = apo.get_body("sun", time) + """ + if not isinstance(time, Time): + time = Time(time) + + _cache_key = _make_cache_key(time, body) + + if not hasattr(self, "_body_cache"): + self._body_cache = {} + + if _cache_key not in self._body_cache: + self._body_cache[_cache_key] = get_body( + body, time, location=self.location, ephemeris=ephemeris + ) + + return self._body_cache[_cache_key] + def altaz(self, time, target=None, obswl=None, grid_times_targets=False): """ Get an `~astropy.coordinates.AltAz` frame or coordinate. @@ -592,9 +685,9 @@ def altaz(self, time, target=None, obswl=None, grid_times_targets=False): """ if target is not None: if target is MoonFlag: - target = get_body("moon", time, location=self.location) + target = self.get_body("moon", time) elif target is SunFlag: - target = get_sun(time) + target = self.get_body("sun", time) time, target = self._preprocess_inputs(time, target, grid_times_targets) @@ -1347,7 +1440,7 @@ def sun_rise_time(self, time, which='nearest', horizon=0*u.degree, n_grid_points >>> print("ISO: {0.iso}, JD: {0.jd}".format(sun_rise)) # doctest: +SKIP ISO: 2001-02-02 14:02:50.554, JD: 2451943.08531 """ - return self.target_rise_time(time, get_sun(time), which, horizon, + return self.target_rise_time(time, self.get_body("sun", time), which, horizon, n_grid_points=n_grid_points) @u.quantity_input(horizon=u.deg) @@ -1398,7 +1491,7 @@ def sun_set_time(self, time, which='nearest', horizon=0*u.degree, n_grid_points= >>> print("ISO: {0.iso}, JD: {0.jd}".format(sun_set)) # doctest: +SKIP ISO: 2001-02-04 00:35:42.102, JD: 2451944.52479 """ - return self.target_set_time(time, get_sun(time), which, horizon, + return self.target_set_time(time, self.get_body("sun", time), which, horizon, n_grid_points=n_grid_points) def noon(self, time, which='nearest', n_grid_points=150): @@ -1427,7 +1520,7 @@ def noon(self, time, which='nearest', n_grid_points=150): `~astropy.time.Time` Time at solar noon """ - return self.target_meridian_transit_time(time, get_sun(time), which, + return self.target_meridian_transit_time(time, self.get_body("sun", time), which, n_grid_points=n_grid_points) def midnight(self, time, which='nearest', n_grid_points=150): @@ -1456,7 +1549,7 @@ def midnight(self, time, which='nearest', n_grid_points=150): `~astropy.time.Time` Time at solar midnight """ - return self.target_meridian_antitransit_time(time, get_sun(time), which, + return self.target_meridian_antitransit_time(time, self.get_body("sun", time), which, n_grid_points=n_grid_points) # Twilight convenience functions @@ -1813,7 +1906,7 @@ def moon_altaz(self, time, ephemeris=None): if not isinstance(time, Time): time = Time(time) - moon = get_body("moon", time, location=self.location, ephemeris=ephemeris) + moon = self.get_body("moon", time, ephemeris=ephemeris) return self.altaz(time, moon) def sun_altaz(self, time): @@ -1842,7 +1935,7 @@ def sun_altaz(self, time): if not isinstance(time, Time): time = Time(time) - sun = get_sun(time) + sun = self.get_body("sun", time) return self.altaz(time, sun) @u.quantity_input(horizon=u.deg) @@ -1952,7 +2045,7 @@ def is_night(self, time, horizon=0*u.deg, obswl=None): if not isinstance(time, Time): time = Time(time) - solar_altitude = self.altaz(time, target=get_sun(time), obswl=obswl).alt + solar_altitude = self.altaz(time, target=self.get_body("sun", time), obswl=obswl).alt if solar_altitude.isscalar: return bool(solar_altitude < horizon)