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

Add tests for ds9 region parser #7

Merged
merged 33 commits into from
Mar 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cfc2ba6
add a minimal ds9 region language specification (that fails)
keflavich Mar 22, 2016
eb9a322
pseudocode parser
keflavich Mar 22, 2016
3b5fb0e
first successful parser!
keflavich Mar 22, 2016
0412154
minor refactor and commenting
keflavich Mar 22, 2016
d6c4eb0
split on semicolons
keflavich Mar 22, 2016
2bda12c
parse polygons!
keflavich Mar 22, 2016
e979dae
parse coordinates as coordinate objects
keflavich Mar 22, 2016
342fe4e
failed to crash!
keflavich Mar 22, 2016
de72fab
10-100x speed boost
keflavich Mar 22, 2016
81c1351
parse the meta too
keflavich Mar 23, 2016
66ae9b9
start turning parsed things into objects
keflavich Mar 23, 2016
e07b04f
now can make objects!
keflavich Mar 23, 2016
edcdf6b
add __init__.py
joleroi Mar 23, 2016
3874972
add test skeleton
joleroi Mar 23, 2016
464c33b
add ds9 writer
joleroi Mar 24, 2016
d6c9ebc
add test
joleroi Mar 24, 2016
24b34bc
add more tests
joleroi Mar 24, 2016
76b597a
handle elliptical annulus correctly and test galactic coords inputs
joleroi Mar 24, 2016
455fa46
add test for physical
joleroi Mar 24, 2016
f0730ba
add coveragerc
joleroi Mar 24, 2016
66e7865
change file names/structure
joleroi Mar 24, 2016
d27dae6
try changing travis grid
keflavich Mar 24, 2016
37a450b
Merge pull request #1 from keflavich/region_tests
joleroi Mar 24, 2016
d148a4f
disable deprecation->exception
keflavich Mar 24, 2016
d7d1ae0
Merge pull request #2 from keflavich/region_tests
joleroi Mar 24, 2016
de602a9
bugfixes
joleroi Mar 25, 2016
9f9c3fd
Merge branch 'region_tests' of https://github.com/joleroi/regions int…
joleroi Mar 25, 2016
6c01e44
add test reading all files also reference files
joleroi Mar 25, 2016
4a89ad3
add meta data to objects
joleroi Mar 25, 2016
2a68201
add profiling scripts
joleroi Mar 25, 2016
ae9e624
fix build error
joleroi Mar 25, 2016
f9883a3
add xfail to tests
joleroi Mar 25, 2016
ab7a4c4
change to looseversion
joleroi Mar 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ env:
# The following versions are the 'default' for tests, unless
# overidden underneath. They are defined here in order to save having
# to repeat them for all configurations.
- NUMPY_VERSION=1.9
- NUMPY_VERSION=1.10
- ASTROPY_VERSION=stable
- SETUP_CMD='test'
- PIP_DEPENDENCIES=''
Expand Down Expand Up @@ -54,6 +54,8 @@ matrix:
env: ASTROPY_VERSION=development

# Try older numpy versions
- python: 2.7
env: NUMPY_VERSION=1.9
- python: 2.7
env: NUMPY_VERSION=1.8
- python: 2.7
Expand Down
2 changes: 1 addition & 1 deletion regions/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Uncomment the following line to treat all DeprecationWarnings as
## exceptions
enable_deprecations_as_exceptions()
#enable_deprecations_as_exceptions()

## Uncomment and customize the following lines to add/remove entries
## from the list of packages for which version numbers are displayed
Expand Down
6 changes: 6 additions & 0 deletions regions/io/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""I/O
"""
from .read_ds9 import *
from .write_ds9 import *

263 changes: 263 additions & 0 deletions regions/io/read_ds9.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst

import string
import itertools
import re
from astropy import units as u
from astropy import coordinates
from ..shapes import circle, rectangle, polygon, ellipse
from ..core import PixCoord


def read_ds9(filename):
region_list = ds9_parser(filename)
return region_list_to_objects(region_list)


def coordinate(string_rep, unit):
# Any ds9 coordinate representation (sexagesimal or degrees)
if 'd' in string_rep or 'h' in string_rep:
return coordinates.Angle(string_rep)
elif unit is 'hour_or_deg':
if ':' in string_rep:
return coordinates.Angle(string_rep, unit=u.hour)
else:
return coordinates.Angle(string_rep, unit=u.deg)
elif unit.is_equivalent(u.deg):
return coordinates.Angle(string_rep, unit=unit)
else:
return u.Quantity(float(string_rep), unit)

unit_mapping = {'"': u.arcsec,
"'": u.arcmin,
'r': u.rad,
'i': u.dimensionless_unscaled,
}


def angular_length_quantity(string_rep):
has_unit = string_rep[-1] not in string.digits
if has_unit:
unit = unit_mapping[string_rep[-1]]
return u.Quantity(float(string_rep[:-1]), unit=unit)
else:
return u.Quantity(float(string_rep), unit=u.deg)

# these are the same function, just different names
radius = angular_length_quantity
width = angular_length_quantity
height = angular_length_quantity
angle = angular_length_quantity

language_spec = {'point': (coordinate, coordinate),
'circle': (coordinate, coordinate, radius),
# This is a special case to deal with n elliptical annuli
'ellipse': itertools.chain((coordinate, coordinate), itertools.cycle((radius, ))),
'box': (coordinate, coordinate, width, height, angle),
'polygon': itertools.cycle((coordinate, )),
}

coordinate_systems = ['fk5', 'fk4', 'icrs', 'galactic', 'wcs', 'physical', 'image', 'ecliptic']
coordinate_systems += ['wcs{0}'.format(letter) for letter in string.ascii_lowercase]

coordsys_name_mapping = dict(zip(coordinates.frame_transform_graph.get_names(),
coordinates.frame_transform_graph.get_names()))
coordsys_name_mapping['ecliptic'] = 'geocentrictrueecliptic' # needs expert attention TODO

hour_or_deg = 'hour_or_deg'
coordinate_units = {'fk5': (hour_or_deg, u.deg),
'fk4': (hour_or_deg, u.deg),
'icrs': (hour_or_deg, u.deg),
'geocentrictrueecliptic': (u.deg, u.deg),
'galactic': (u.deg, u.deg),
'physical': (u.dimensionless_unscaled, u.dimensionless_unscaled),
'image': (u.dimensionless_unscaled, u.dimensionless_unscaled),
'wcs': (u.dimensionless_unscaled, u.dimensionless_unscaled),
}
for letter in string.ascii_lowercase:
coordinate_units['wcs{0}'.format(letter)] = (u.dimensionless_unscaled, u.dimensionless_unscaled)

region_type_or_coordsys_re = re.compile("^#? *(-?)([a-zA-Z0-9]+)")

paren = re.compile("[()]")

def strip_paren(string_rep):
return paren.sub("", string_rep)


def region_list_to_objects(region_list):
viz_keywords = ['color', 'dashed', 'width', 'point', 'font', 'text']

output_list = []
for region_type, coord_list, meta in region_list:
#print("region_type, region_type is 'circle', type(region_type), type('circle'), id(region_type), id('circle'), id(str(region_type))")
#print(region_type, region_type is 'circle', type(region_type), type('circle'), id(region_type), id('circle'), id(str(region_type)))
if region_type == 'circle':
if isinstance(coord_list[0], coordinates.SkyCoord):
reg = circle.CircleSkyRegion(coord_list[0], coord_list[1])
elif isinstance(coord_list[0], PixCoord):
reg = circle.CirclePixelRegion(coord_list[0], coord_list[1])
else:
raise ValueError("No central coordinate")
elif region_type == 'ellipse':
# Do not read elliptical annuli for now
if len(coord_list) > 4:
continue
if isinstance(coord_list[0], coordinates.SkyCoord):
reg = ellipse.EllipseSkyRegion(coord_list[0], coord_list[1], coord_list[2], coord_list[3])
elif isinstance(coord_list[0], PixCoord):
reg = ellipse.EllipsePixelRegion(coord_list[0], coord_list[1], coord_list[2], coord_list[3])
else:
raise ValueError("No central coordinate")
elif region_type == 'polygon':
if isinstance(coord_list[0], coordinates.SkyCoord):
reg = polygon.PolygonSkyRegion(coord_list[0])
elif isinstance(coord_list[0], PixCoord):
reg = polygon.PolygonPixelRegion(coord_list[0])
else:
raise ValueError("No central coordinate")
elif region_type == 'rectangle':
if isinstance(coord_list[0], coordinates.SkyCoord):
reg = rectangle.RectangleSkyRegion(coord_list[0], coord_list[1], coord_list[2], coord_list[3])
elif isinstance(coord_list[0], PixCoord):
reg = rectangle.RectanglePixelRegion(coord_list[0], coord_list[1], coord_list[2], coord_list[3])
else:
raise ValueError("No central coordinate")
else:
continue
reg.vizmeta = {key: meta[key] for key in meta.keys() if key in viz_keywords}
reg.meta = {key: meta[key] for key in meta.keys() if key not in viz_keywords}
output_list.append(reg)
return output_list


def ds9_parser(filename):
"""
Parse a complete ds9 .reg file

Returns
-------
list of (region type, coord_list, meta) tuples
"""
coordsys = None
regions = []
composite_region = None

with open(filename,'r') as fh:
for line_ in fh:
# ds9 regions can be split on \n or ;
for line in line_.split(";"):
parsed = line_parser(line, coordsys)
if parsed in coordinate_systems:
coordsys = parsed
elif parsed:
region_type, coordlist, meta, composite = parsed
if composite and composite_region is None:
composite_region = [(region_type, coordlist)]
elif composite:
composite_region.append((region_type, coordlist))
elif composite_region is not None:
composite_region.append((region_type, coordlist))
regions.append(composite_region)
composite_region = None
else:
regions.append((region_type, coordlist, meta))

return regions


def line_parser(line, coordsys=None):
region_type_search = region_type_or_coordsys_re.search(line)
if region_type_search:
include = region_type_search.groups()[0]
region_type = region_type_search.groups()[1]
else:
return

if region_type in coordinate_systems:
return region_type # outer loop has to do something with the coordinate system information
elif region_type in language_spec:
if coordsys is None:
raise ValueError("No coordinate system specified and a region has been found.")

if "||" in line:
composite = True
else:
composite = False

# end_of_region_name is the coordinate of the end of the region's name, e.g.:
# circle would be 6 because circle is 6 characters
end_of_region_name = region_type_search.span()[1]
# coordinate of the # symbol or end of the line (-1) if not found
hash_or_end = line.find("#")
coords_etc = strip_paren(line[end_of_region_name:hash_or_end].strip(" |"))
meta_str = line[hash_or_end:]

parsed_meta = meta_parser(meta_str)

if coordsys in coordsys_name_mapping:
parsed = type_parser(coords_etc, language_spec[region_type],
coordsys_name_mapping[coordsys])

# Reset iterator for ellipse annulus
if region_type == 'ellipse':
language_spec[region_type] = itertools.chain((coordinate, coordinate), itertools.cycle((radius, )))

coords = coordinates.SkyCoord([(x, y)
for x, y in zip(parsed[:-1:2], parsed[1::2])
if isinstance(x, coordinates.Angle) and
isinstance(x, coordinates.Angle)], frame=coordsys_name_mapping[coordsys])

return region_type, [coords] + parsed[len(coords)*2:], parsed_meta, composite
else:
parsed = type_parser(coords_etc, language_spec[region_type],
coordsys)
if region_type == 'polygon':
# have to special-case polygon in the phys coord case b/c can't typecheck when iterating as in sky coord case
coord = PixCoord(parsed[0::2], parsed[1::2])
parsed_return = [coord]
else:
parsed = [_.value for _ in parsed]
coord = PixCoord(parsed[0], parsed[1])
parsed_return = [coord]+parsed[2:]

# Reset iterator for ellipse annulus
if region_type == 'ellipse':
language_spec[region_type] = itertools.chain((coordinate, coordinate), itertools.cycle((radius, )))

return region_type, parsed_return, parsed_meta, composite


def type_parser(string_rep, specification, coordsys):
coord_list = []
splitter = re.compile("[, ]")
for ii, (element, element_parser) in enumerate(zip(splitter.split(string_rep), specification)):
if element_parser is coordinate:
unit = coordinate_units[coordsys][ii % 2]
coord_list.append(element_parser(element, unit))
else:
coord_list.append(element_parser(element))

return coord_list


# match an x=y pair (where y can be any set of characters) that may or may not
# be followed by another one
meta_token = re.compile("([a-zA-Z]+)(=)([^= ]+) ?")

#meta_spec = {'color': color,
# }
# global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1
# ruler(+175:07:14.900,+50:56:21.236,+175:06:52.643,+50:56:11.190) ruler=physical physical color=white font="helvetica 12 normal roman" text={Ruler}



def meta_parser(meta_str):
meta_token_split = [x for x in meta_token.split(meta_str.strip()) if x]
equals_inds = [i for i, x in enumerate(meta_token_split) if x is '=']
result = {meta_token_split[ii-1]:
" ".join(meta_token_split[ii+1:jj-1 if jj is not None else None])
for ii,jj in zip(equals_inds, equals_inds[1:]+[None])}

return result

6 changes: 6 additions & 0 deletions regions/io/setup_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
import os

def get_package_data():
parser_test = ['data/*.reg']
return {'regions.io.tests': parser_test}
4 changes: 4 additions & 0 deletions regions/io/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
This packages contains affiliated package tests.
"""
10 changes: 10 additions & 0 deletions regions/io/tests/data/ds9.color.reg
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Region file format: DS9 version 4.1
global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1
fk5
circle(13:29:52.675,+47:11:45.02,1") # color=blue
circle(13:29:52.675,+47:11:45.02,2") # color=#800
circle(13:29:52.675,+47:11:45.02,3") # color=#0a0
circle(13:29:52.675,+47:11:45.02,4") # color=#880000
circle(13:29:52.675,+47:11:45.02,5") # color=#00aa00
circle(13:29:52.675,+47:11:45.02,6") # color=#888800000000
circle(13:29:52.675,+47:11:45.02,7") # color=#0000aaaa0000
10 changes: 10 additions & 0 deletions regions/io/tests/data/ds9.comment.reg
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Region file format: DS9 version 4.0
#
# foobar
# foobar+
# foo+bar
## foobar
### foobar
#. foobar
##. foobar

39 changes: 39 additions & 0 deletions regions/io/tests/data/ds9.composite.reg
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Region file format: DS9 version 4.1
global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1
fk5
# composite(202.48165,47.1931,317.39831) || composite=1
circle(202.48705,47.208237,3.9640007") || # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment
-ellipse(202.48309,47.204492,7.9280014",3.9640007",2.3983109) || # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background
-box(202.47783,47.201057,15.856003",7.9280014",2.3983109) || # color=yellow font="helvetica 10 bold roman" text={Box}
polygon(202.47309,47.198922,202.46877,47.199044,202.46859,47.196108,202.47291,47.195985) || # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0
-line(202.4684,47.194116,202.46202,47.193946) || # line=1 1 color=cyan text={Line}
# vector(202.46159,47.191504,7.9280014",2.3983109) || vector=1 color=red text={Vector}
# text(202.45907,47.188886) || color=magenta font="helvetica 14 bold roman" text={Region}
# ruler(202.48176,47.194165,202.47471,47.194171) || ruler=physical physical color=white font="helvetica 12 normal roman" text={Ruler}
annulus(202.49319,47.203697,1.9820003",3.9640007",5.946001") || # color=magenta font="helvetica 10 bold roman" text={Annulus}
ellipse(202.48834,47.200368,2.9730005",1.4865003",5.946001",2.9730005",2.3983109) || # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus}
box(202.4832,47.197085,7.9280014",3.9640007",11.892002",5.946001",2.3983109) || # font="helvetica 10 bold roman" text={Box Annulus}
point(202.49871,47.200189) || # point=circle text={Circle Point}
point(202.49351,47.196791) || # point=box color=red width=3 text={Box Point}
point(202.48857,47.193854) || # point=diamond text={Diamond Point}
point(202.5005,47.197643) || # point=cross color=blue text={Cross Point}
point(202.49591,47.194852) || # point=x text={X Point}
point(202.49148,47.191862) || # point=arrow color=magenta text={Arrow Point}
point(202.49864,47.19267) || # point=boxcircle text={BoxCircle Point}
# projection(202.47611,47.189884,202.46694,47.189092,3.9640007") || text={Projection}
panda(202.48266,47.190165,317.39831,587.39831,3,0",5.946001",2) || # text={Panda}
panda(202.48753,47.186403,8.9802109,47.398311,1,0",2.9730005",1) || # panda=(8.9802109 47.398311 137.39831 227.39831)(0" 2.9730005" 5.946001") text={Panda 2}
panda(202.48753,47.186403,8.9802109,47.398311,1,2.9730005",5.946001",1) || # panda=ignore
panda(202.48753,47.186403,47.398311,137.39831,1,0",2.9730005",1) || # panda=ignore
panda(202.48753,47.186403,47.398311,137.39831,1,2.9730005",5.946001",1) || # panda=ignore
panda(202.48753,47.186403,137.39831,227.39831,1,0",2.9730005",1) || # panda=ignore
panda(202.48753,47.186403,137.39831,227.39831,1,2.9730005",5.946001",1) || # panda=ignore
# compass(202.46768,47.186188,7.9280014") || compass=physical {N} {E} 1 1 text={Compass}
epanda(202.47821,47.186785,317.39831,587.39831,3,2.9730005",1.4865003",5.946001",2.9730005",1,2.3983109) || # text={Epanda}
epanda(202.48291,47.183066,2.3983109,47.398311,1,2.9730005",1.4865003",5.946001",2.9730005",1,2.3983109) || # epanda=(2.3983109 47.398311 137.39831 227.39831)(2.9730005" 1.4865003" 5.946001" 2.9730005")(2.3983109) text={Epanda 2}
epanda(202.48291,47.183066,47.398311,137.39831,1,2.9730005",1.4865003",5.946001",2.9730005",1,2.3983109) || # epanda=ignore
epanda(202.48291,47.183066,137.39831,227.39831,1,2.9730005",1.4865003",5.946001",2.9730005",1,2.3983109) || # epanda=ignore
bpanda(202.47302,47.183543,317.39831,587.39831,3,7.9280014",3.9640007",11.892002",5.946001",1,2.3983109) || # text={Bpanda}
bpanda(202.47809,47.180126,2.3983109,47.398311,1,7.9280014",3.9640007",11.892002",5.946001",1,2.3983109) # bpanda=(2.3983109 47.398311 137.39831 227.39831)(7.9280014" 3.9640007" 11.892002" 5.946001")(2.3983109) text={Bpanda 2}
bpanda(202.47809,47.180126,47.398311,137.39831,1,7.9280014",3.9640007",11.892002",5.946001",1,2.3983109) # bpanda=ignore
bpanda(202.47809,47.180126,137.39831,227.39831,1,7.9280014",3.9640007",11.892002",5.946001",1,2.3983109) # bpanda=ignore