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

Beginning in the FixedTarget and Observer classes #11

Merged
merged 12 commits into from
Jun 26, 2015
4 changes: 2 additions & 2 deletions astroplan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
if not _ASTROPY_SETUP_:
from .utils import *

# Import everything in astroplan.core
from .core import *
# Import everything in astroplan.core
from .core import *
171 changes: 130 additions & 41 deletions astroplan/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
from __future__ import (absolute_import, division, print_function,
unicode_literals)

from astropy.coordinates import (EarthLocation, Latitude, Longitude, SkyCoord,
Copy link
Member

Choose a reason for hiding this comment

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

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 don't either, maybe @eteq can give us some hints on how to read these logs on Monday or in the Hangout on Tuesday.

Copy link
Member

Choose a reason for hiding this comment

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

I still don't understand why that line gave an error on travis-ci:

ImportError: No module named astropy.coordinates

But if you fix the other errors mentioned below and python setup.py test (which imports this file) works locally, it should work on travis-ci, too.

Copy link
Member

Choose a reason for hiding this comment

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

Just adding a line note to remind us that this was resolved by a later commit -- the issue was a missing indentation in astroplan/__init__.py for a line that was importing from astroplan.core.

AltAz)
import astropy.units as u
from astropy.units import Quantity

import pytz

################################################################################
# TODO: Temporary solution to IERS tables problems
from astropy.utils.data import download_file
from astropy.utils import iers
import datetime
iers.IERS.iers_table = iers.IERS_A.open(download_file(iers.IERS_A_URL, cache=True))
################################################################################

#from ..extern import six

#import sys
Expand All @@ -25,82 +40,137 @@ class Observer(object):
Some comments.
"""

def __init__(self, name, longitude, latitude, elevation, pressure,
relative_humidity, temperature, timezone, description=None):
def __init__(self, name=None, location=None, latitude=None, longitude=None,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we instead use the **kwargs syntax and then kwargs.get or kwargs.pop the keywords one at a time?

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 my preference is no to kwargs -- if the arguments are spelled out, it is easier to quickly see what is expected from the documentation of the call sequence.

Copy link
Member

Choose a reason for hiding this comment

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

👍 to @adrn here. Also makes it easier to see what the default is.

elevation=None, timezone='UTC', pressure=None,
relative_humidity=None, temperature=None, description=None):
"""
Initializes an Observer object.

<longer description>
TODO: <longer description>

Parameters
----------
name : str
A short name for the telescope, observatory or location.

longitude : str or `~astropy.units.quantity`?
The longitude of the observing location.
pressure : `~astropy.units.Quantity`
The ambient pressure.

latitude : str or `~astropy.units.quantity`?
The latitude of the observing location.
location : `~astropy.coordinates.EarthLocation`
The location (latitude, longitude, elevation) of the observatory.

longitude : str or `~astropy.units.Quantity`
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add (optional) after ~astropy.units.Quantity, and in the first line of the description specify that this is not the preferred method for instantiating an Observer (e.g., use location=). Same below for latitude and elevation.

The longitude of the observing location. If str, should be a string
that initializes a `~astropy.coordinates.Longitude` object with
units in degrees.

latitude : str or `~astropy.units.Quantity`
The latitude of the observing location. If str, should be a string
that initializes a `~astropy.coordinates.Latitude` object with
units in degrees.

elevation : `~astropy.units.Quantity`
The elevation of the observing location, with respect to sea
level.

pressure : `~astropy.units.Quantity`
The ambient pressure.

relative_humidity : float
The ambient relative humidity.

temperature : `~astropy.units.Quantity`
The ambient temperature.

timezone : WHAT TYPE IS pytz.timezone ?
The local timezone.
timezone : str or `datetime.tzinfo`
The local timezone, as either an instance of `pytz.timezone()` or
Copy link
Member

Choose a reason for hiding this comment

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

The way you've phrased it here you're duplicating information because you're just saying the type. Maybe instead "The local timezone to assume. If a string, it will be passed through pytz.timezone() to produce the timezone object.

the string accepted by `pytz.timezone()`.

description : str
A short description of the telescope, observatory or observing
location.
"""
raise NotImplementedError()

def set_environment(self, pressure, relative_humidity, temperature):
"""
Updates the Observer object with environmental conditions.
if name is not None:
self.name = name

<longer description>
if pressure is None:
pressure = 0

Parameters
----------
pressure : `~astropy.units.Quantity`
The ambient pressure.
if temperature is None:
temperature = 0 * u.deg_C
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure we want to set the default (unset) values to 0 -- this is a valid number that will work in arithmetic operations but may return weird results. I think we want them to be None if they are unspecified. Here's an example: imagine a user forgets to specify temperature, and later on we add a method that internally uses the temperature for some calculation. Unless we check for 0, the calculation will proceed, but may return nonsense. I think we want to just check for None values...

Copy link
Member

Choose a reason for hiding this comment

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

Another way to think about it: there is a conceptual difference between "temperature=0" and "temperature is unspecified"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@adrn – The place where that became handy for me was in the altaz method where I copy over the environment instance variables to initialize an AltAz frame. How would be the best way to handle that if self.temperature = None?

Copy link
Member

Choose a reason for hiding this comment

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

If I recall, AltAz has its own internal default assumptions, so let that class deal with this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What's the right pattern for calling AltAz() with only the kwargs that have values other than None? Would you do a loop through possible keywords and search for ones that are not None and then pass those into AltAz()?


relative_humidity : `~astropy.units.Quantity`
The ambient relative humidity.
if relative_humidity is None:
relative_humidity = 0

temperature :
The ambient temperature.
"""
raise NotImplementedError()
self.pressure = pressure
self.temperature = temperature
self.relative_humidity = relative_humidity

def get_date(self, input_date_time, timezone=None):
"""
Builds an object containing date and time information.
# If lat/long given instead of EarthLocation, convert them
# to EarthLocation
if location is None and (latitude is not None and longitude is not None):
accepted_latlon_types = [str, Quantity]
if (type(latitude) in accepted_latlon_types and
Copy link
Member

Choose a reason for hiding this comment

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

Maybe I'm wrong here (@eteq let me know), but is this necessary? The initializer for EarthLocation should do something similar internally, so we may want to just pass the latitude and longitude directly in to the initializer and let that handle this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seemed to me from the EarthLocation docs that EarthLocation.from_geodetic takes lat/long as either Latitudes/Longitudes or floats, whereas Latitude/Longitude have very flexible inputs, which excited me.

Copy link
Member

Choose a reason for hiding this comment

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

Works with EarthLocation too -- internally, it's probably passing the input in to let Latitude and Longitude classes do the parsing. Encapsulation!

e.g., this works:

loc = coord.EarthLocation.from_geodetic(lon="150:21:41.1", lat=45*u.deg, height=15*u.m)

Copy link
Member

Choose a reason for hiding this comment

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

(that is, you're just repeating code that already exists in the EarthLocation initializer)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Awesome, maybe this is an indication that the EarthLocation docs can be updated to specify their awesome input flexibility :)

Copy link
Member

Choose a reason for hiding this comment

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

Actually, it's useful for developers to know, but I think it is not in the docs because we want to encourage users to use quantities or objects (rather than strings! strings are baaaad! bad!)

Copy link
Member

Choose a reason for hiding this comment

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

(willing to reconsider that decision, if that was a decision and not just an oversight in the first place..)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean, I'm an insider now? 😄

type(longitude) in accepted_latlon_types):
latitude = Latitude(latitude, unit=u.degree)
longitude = Longitude(longitude, unit=u.degree)

self.location = EarthLocation.from_geodetic(longitude, latitude,
elevation)

elif isinstance(location, EarthLocation):
self.location = location
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is my compromise between @ejeschke 's accepting attitude towards input formats and @adrn 's preference for accepting one type - EarthLocation. I accept latitudes and longitudes or an EarthLocation. If you input latitude/longitude, they become an EarthLocation under the hood.


Default time zone is UTC.
If time zone (local or otherwise) requested, then uses `datetime.datetime`
and pytz.timezone to convert from UTC.
else:
raise TypeError('Observatory location must be specified with '
Copy link
Member

Choose a reason for hiding this comment

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

Make me happy, and put EarthLocation as the 1st option here :) -- we want to encourage the OOP!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Anything to make you happy, @adrn!

'either (1) latitude and longitude in degrees as '
'accepted by astropy.coordinates.Latitude and '
'astropy.coordinates.Latitude, or (2) as an '
'instance of astropy.coordinates.EarthLocation.')

Must first create date_time object with get_date() before using any other
Observer methods, all of which require one as an argument.
# Accept various timezone inputs, default to UTC
if isinstance(timezone, datetime.tzinfo):
self.timezone = timezone
elif isinstance(timezone, str) or isinstance(timezone, unicode):
self.timezone = pytz.timezone(timezone)
else:
raise TypeError('timezone keyword should be a string, or an '
'instance of datetime.tzinfo')

def altaz(self, time, target=None, obswl=None):
"""
Returns an instance of `~astropy.coordinates.SkyCoord` with altitude and
azimuth of the `FixedTarget` called `target` at time `time`.

Parameters
----------
input_date_time : WHAT TYPE IS astropy.time OBJECT?
time : `~astropy.time.Time`
Astropy time object.
Copy link
Member

Choose a reason for hiding this comment

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

This is a repeat of the type information. Instead maybe The time at which the observation is taking place. Will be used as the obstime attribute in the resulting frame or coordinate.


target : None (default) or `~astroplan.FixedTarget` or `~astropy.coordinates.SkyCoord`
Celestial object of interest. If `target`=None, return just the
`~astropy.coordinates.AltAz` frame without coordinates.

Copy link
Member

Choose a reason for hiding this comment

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

Add obswl to docstring?

Copy link
Member

Choose a reason for hiding this comment

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

Actually, I think this should go in the Observer class. Everything else that goes into creating AltAz does, so it makes sense that obswl would, too. (but if it doesn't, then I agree with @adrn it needs to be there)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My strategy was that the Observer should really encapsulate just the observatory. Since you could observe multiple targets with the same observatory, or the same target at multiple wavelengths, I think we should keep obswl confined to specific calculations like obs.altaz().

"""
raise NotImplementedError()
if obswl is None:
obswl = 1*u.micron

altaz_frame = AltAz(location=self.location, obstime=time,
pressure=self.pressure, obswl=obswl,
temperature=self.temperature,
relative_humidity=self.relative_humidity)

if target is None:
return altaz_frame
else:
if not (isinstance(target, FixedTarget) or
isinstance(target, SkyCoord)):
raise TypeError('The target must be an instance of FixedTarget '
'or SkyCoord.')

if hasattr(target, 'coord'):
coordinate = target.coord
else:
coordinate = target
return coordinate.transform_to(altaz_frame)

# Sun-related methods.

Expand Down Expand Up @@ -273,7 +343,7 @@ class Target(object):
"""
__metaclass__ = ABCMeta

def __init__(self, name, ra, dec, marker=None):
def __init__(self, name=None, ra=None, dec=None, marker=None):
"""
Defines a single observation target.

Expand All @@ -296,21 +366,40 @@ def ra(self):
"""
Right ascension.
"""
raise NotImplementedError
if isinstance(self, FixedTarget):
return self.coord.ra
raise NotImplementedError()

@property
def dec(self):
"""
Declination.
"""
raise NotImplementedError
if isinstance(self, FixedTarget):
return self.coord.dec
raise NotImplementedError()


class FixedTarget(Target):
"""
An object that is "fixed" with respect to the celestial sphere.
"""

def __init__(self, coord, **kwargs):
'''
TODO: Docstring.
'''
if not isinstance(coord, SkyCoord):
Copy link
Member

Choose a reason for hiding this comment

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

This should probably accept a SkyCoord or any of the BaseCoordinateFrame classes? (e.g., ICRS)

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 haven't played much with the other BaseCoordinateFrame subclasses – could generalizing introduce any subtleties that could cause errors I might not be thinking of?

raise TypeError('Coordinate must be a SkyCoord.')
Copy link
Member

Choose a reason for hiding this comment

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

Oops, you don't want to do this. We do want to allow people to pass in the "low-level" frame classes like ICRS or AltAz or whatever. You want some kind of validation, though. Perhaps the thing to do is to check for a few methods that really should be there? Like tranform_to and represent_as? This follows the python tradition of "duck-typing": when you see a bird that walks like a duck and swims like a duck and quacks like a duck, you call that bird a duck. Similar with coordinates.


self.name = kwargs.get('name', None)
Copy link
Member

Choose a reason for hiding this comment

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

make name an explicit keyword argument?

self.coord = coord

@classmethod
def from_name(cls, query_name, **kwargs):
# Allow manual override for name keyword so that the target name can
# be different from the query name, otherwise assume name=queryname.
name = kwargs.pop('name', query_name)
Copy link
Member

Choose a reason for hiding this comment

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

add target_name or something as an optional but explicit keyword argument?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to having an explicit keyword arg for name!

return cls(SkyCoord.from_name(query_name), name=name, **kwargs)

class NonFixedTarget(Target):
"""
Expand Down
Loading