Skip to content

Instantly share code, notes, and snippets.

@alexrudy
Last active August 29, 2015 14:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexrudy/49ded8844bb86ff867af to your computer and use it in GitHub Desktop.
Save alexrudy/49ded8844bb86ff867af to your computer and use it in GitHub Desktop.
Starlist Parsing Tools
"""
This is a parser for the Keck starlist format.
The format is documented at the Keck website: <https://www2.keck.hawaii.edu/observing/starlist.html>
Author: Alex Rudy, UCSC
Date: 2015-01-24
License: BSD
"""
import warnings
from datetime import date, datetime
import astropy.units as u
import astropy.time
from astropy.coordinates import SkyCoord, FK4, FK5, AltAz
import re
_starlist_re_raw = r"""
^(?P<Name>.{1,15})[\s]+ # Target name must be the first 15 characters.
(?P<RA>(?:[\d]{1,2}[\s:][\s\d]?[\d][\s:][\s\d]?[\d](?:\.[\d]+)?)|(?:[\d]+\.[\d]+))[\s]+ # Right Ascension, HH MM SS.SS+
(?P<Dec>(?:[+-]?[\d]{1,2}[\s:][\s\d]?[\d][\s:][\s\d]?[\d](?:\.[\d]+)?)|(?:[\d]+\.[\d]+)) # Declination, (-)DD MM SS.SS+
(?:[\s]+(?P<Equinox>(?:[\d]{4}(?:\.[\d]+)?)|(?:APP)))?[\s]* # Equinox.
(?P<Keywords>.+)?$ # Everything else must be a keyword.
"""
_starlist_re = re.compile(_starlist_re_raw, re.VERBOSE)
_starlist_re_strict = r"""
^(?P<Name>.{15})\ # Target name must be the first 15 characters.
(?P<RA>[\d]{2}\ [\d]{2}\ [\d]{2}(?:\.[\d]+)?)\ # Right Ascension, HH MM SS.SS+
(?P<Dec>[+-]?[\d]{1,2}\ [\d]{2}\ [\d]{2}(?:\.[\d]+)?)\ # Declination, (-)DD MM SS.SS+
(?P<Equinox>(?:[\d]{4}(?:\.[\d]+)?)|(?:APP))[\ ]? # Equinox.
(?P<Keywords>[^\ ].+)?$ # Everything else must be a keyword.
"""
_starlist_token_parts = ["Name", "RA", "Dec", "Equinox", "Keywords"]
def verify_starlist_line(text, identifier="<stream line 0>", warning=False):
"""Verify that the given line is a valid starlist."""
line = text
messages = []
match = _starlist_re.match(text)
if not match:
raise ValueError("Couldn't parse '{0:s}', no regular expression match found.".format(text))
data = match.groupdict("")
# Check the Name:
name_length = match.end('Name') - match.start('Name') + 1
if name_length < 15:
messages.append(('Warning','Name','Name should be exactly 15 characters long (whitespace is ok.) len(Name)={0:d}'.format(name_length)))
# Check the RA starting position.
if match.start('RA') + 1 != 17:
messages.append(('Error','RA','RA must start in column 17. Start: {0:d}'.format(match.start('RA')+1)))
# Check the Dec starting token
if match.start('Dec') - match.end('RA') != 1:
messages.append(('Warning','Dec','RA and Dec should be separated by only a single space, found {0:d} characters.'.format(match.start('Dec') - match.end('RA'))))
# Check the Equinox starting token.
if match.start('Equinox') - match.end('Dec') != 1:
messages.append(('Warning','Equinox','Dec and Equinox should be separated by only a single space, found {0:d} characters.'.format(match.start('Equinox') - match.end('Dec'))))
if match.group("Keywords") and len(match.group("Keywords")):
for kwarg in match.group("Keywords").split():
if kwarg.count("=") < 1:
messages.append(('Error', 'Keywords', 'Each keyword/value pair must have 1 "=", none found {!r}'.format(kwarg)))
if kwarg.count("=") > 1:
messages.append(('Error', 'Keywords', 'Each keyword/value pair must have 1 "=", {0:d} found {1!r}'.format(kwarg.count("="), kwarg)))
for severity, token, message in messages:
composed_message = "[{0}][{1} {2}] {3}".format(severity, identifier, token, message)
if warning:
warnings.warn(composed_message)
else:
print(composed_message)
def parse_starlist_line(text):
"""Parse a single line from a Keck formatted starlist, returning a dictionary of parsed values.
:param text: The starlist text line.
:returns: A dictionary of starlist object properties, set from teh starlist line.
:raises: ValueError if the line couldn't be parsed.
This function parses a single line from a starlist and returns a dictionary of items from that line. The followig keys are included:
- `Name`: The target name.
- `Position`: An astropy.coordinates object representing this position.
- Any other keyword/value pairs, which are found at the end of the starlist line, and formatted as ``keyword=value``
"""
match = _starlist_re.match(text)
if not match:
raise ValueError("Couldn't parse '{}', no regular expression match found.".format(text))
data = match.groupdict("")
if data.get('Equinox','') == '':
equinox = astropy.time.Time.now()
frame = AltAz
elif data['Equinox'] == "APP":
equinox = astropy.time.Time.now()
frame = 'fk5'
else:
equinox = astropy.time.Time(float(data['Equinox']), format='jyear', scale='utc')
if float(data['Equinox']) <= 1950:
equinox = astropy.time.Time(float(data['Equinox']), format='byear', scale='utc')
frame = 'fk4'
else:
frame = 'fk5'
position = SkyCoord(data["RA"], data["Dec"], unit=(u.hourangle, u.degree), equinox=equinox, frame=frame)
results = dict(
Name = data['Name'].strip(),
Position = position,
)
for keywordvalue in data.get("Keywords","").split():
if keywordvalue.count("=") < 1:
warnings.warn("Illegal Keyword Argument: '{}'".format(keywordvalue))
continue
keyword, value = keywordvalue.split("=",1)
keyword = keyword.strip()
if keyword in set(["Name", "Position"]):
warnings.warn("Illegal Keyword Name: '{}'".format(keyword))
results[keyword] = value.strip()
return results
def read_skip_comments(filename, comments="#"):
"""Read a filename, yielding lines that don't start with comments."""
with open(filename, 'r') as stream:
for line in stream:
if not line.startswith(comments):
yield line.strip().strip("\n\r")
def parse_starlist(starlist):
"""Parse a starlist into a sequence of dictionaries."""
for line in read_skip_comments(starlist):
yield parse_starlist_line(line)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment