Skip to content

Commit

Permalink
Merge pull request #7 from joleroi/region_tests
Browse files Browse the repository at this point in the history
Add tests for ds9 region parser
  • Loading branch information
astrofrog committed Mar 25, 2016
2 parents 2ba24ea + ab7a4c4 commit 95532a6
Show file tree
Hide file tree
Showing 68 changed files with 3,042 additions and 11 deletions.
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

0 comments on commit 95532a6

Please sign in to comment.