Skip to content

Commit cf8e572

Browse files
committed
make SkyCoordBuilder able to process MangoObject instances
To be continued accordingly to the MANGO recommandation process
1 parent 44aeee5 commit cf8e572

File tree

4 files changed

+317
-50
lines changed

4 files changed

+317
-50
lines changed

pyvo/mivot/features/sky_coord_builder.py

Lines changed: 128 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
"""
33
Utility transforming MIVOT annotation into SkyCoord instances
44
"""
5-
5+
import numbers
66
from astropy.coordinates import SkyCoord
77
from astropy import units as u
88
from astropy.coordinates import ICRS, Galactic, FK4, FK5
9-
from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError
9+
from astropy.time.core import Time
10+
from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError, MappingError
1011

1112

1213
class MangoRoles:
@@ -62,7 +63,9 @@ def __init__(self, mivot_instance_dict):
6263
def build_sky_coord(self):
6364
"""
6465
Build a SkyCoord instance from the MivotInstance dictionary.
65-
The operation requires the dictionary to have ``mango:EpochPosition`` as dmtype
66+
The operation requires the dictionary to have ``mango:EpochPosition`` as dmtype.
67+
This instance can be either the root of the dictionary or it can be one
68+
of the Mango properties if the root object is a mango:MangoObject instance
6669
This is a public method which could be extended to support other dmtypes.
6770
6871
returns
@@ -75,15 +78,28 @@ def build_sky_coord(self):
7578
NoMatchingDMTypeError
7679
if the SkyCoord instance cannot be built.
7780
"""
78-
if self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:EpochPosition":
81+
82+
if self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:MangoObject":
83+
property_dock = self._mivot_instance_dict["propertyDock"]
84+
for mango_property in property_dock:
85+
if mango_property["dmtype"] == "mango:EpochPosition":
86+
self._mivot_instance_dict = mango_property
87+
return self._build_sky_coord_from_mango()
88+
raise NoMatchingDMTypeError(
89+
"No INSTANCE with dmtype='mango:EpochPosition' has been found:"
90+
" in the property dock of the MangoObject, "
91+
"cannot build a SkyCoord from annotations")
92+
93+
elif self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:EpochPosition":
7994
return self._build_sky_coord_from_mango()
8095
raise NoMatchingDMTypeError(
8196
"No INSTANCE with dmtype='mango:EpochPosition' has been found:"
8297
" cannot build a SkyCoord from annotations")
8398

84-
def _set_year_time_format(self, hk_field, besselian=False):
99+
def _get_time_instance(self, hk_field, besselian=False):
85100
"""
86101
Format a date expressed in year as [scale]year
102+
- Exception possibly risen by Astropy are not caught
87103
88104
parameters
89105
----------
@@ -94,33 +110,96 @@ def _set_year_time_format(self, hk_field, besselian=False):
94110
95111
returns
96112
-------
97-
string or None
98-
attribute value formatted as [scale]year
113+
Time instance or None
114+
115+
raise
116+
-----
117+
MappingError: if the Time instance cannot be built for some reason
99118
"""
100-
scale = "J" if not besselian else "B"
101119
# Process complex type "mango:DateTime
102-
# only "year" representation are supported yet
103120
if hk_field['dmtype'] == "mango:DateTime":
104121
representation = hk_field['representation']['value']
105122
timestamp = hk_field['dateTime']['value']
106-
if representation == "year":
107-
return f"{scale}{timestamp}"
123+
# Process simple attribute
124+
else:
125+
representation = hk_field.get("unit")
126+
timestamp = hk_field.get("value")
127+
128+
if not representation or not timestamp:
129+
raise MappingError(f"Cannot interpret field {hk_field} "
130+
f"as a {('besselian' if besselian else 'julian')} timestamp")
131+
132+
time_instance = self. _build_time_instance(timestamp, representation, besselian)
133+
if not time_instance:
134+
raise MappingError(f"Cannot build a Time instance from {hk_field}")
135+
136+
return time_instance
137+
138+
def _build_time_instance(self, timestamp, representation, besselian=False):
139+
"""
140+
Build a Time instance matching the input parameters.
141+
- Returns None if the parameters do not allow any Time setup
142+
- Exception possibly risen by Astropy are not caught at this level
143+
144+
parameters
145+
----------
146+
timestamp: string or number
147+
The timestamp must comply with the given representation
148+
representation: string
149+
year, iso, ... (See MANGO primitive types derived from ivoa:timeStamp)
150+
besselian: boolean (optional)
151+
Flag telling to use the besselain calendar. We assume it to only be
152+
relevant for FK5 frame
153+
returns
154+
-------
155+
Time instance or None
156+
"""
157+
if representation in ["year", "yr"]:
158+
# it the timestamp is numeric, we infer its format from the besselian flag
159+
if isinstance(timestamp, numbers.Number):
160+
return Time(f"{('B' if besselian else 'J')}{timestamp}",
161+
format=("byear_str" if besselian else "jyear_str"))
162+
if besselian:
163+
if timestamp.startswith("B"):
164+
return Time(f"{timestamp}", format="byear_str")
165+
elif timestamp.startswith("J"):
166+
# a besselain year cannot be given as "Jxxxx"
167+
return None
168+
elif timestamp.isnumeric():
169+
# we force the string representation not to break the test assertions
170+
return Time(f"B{timestamp}", format="byear_str")
171+
else:
172+
if timestamp.startswith("J"):
173+
return Time(f"{timestamp}", format="jyear_str")
174+
elif timestamp.startswith("B"):
175+
# a julian year cannot be given as "Bxxxx"
176+
return None
177+
elif timestamp.isnumeric():
178+
# we force the string representation not to break the test assertions
179+
return Time(f"J{timestamp}", format="jyear_str")
180+
# no case matches
108181
return None
109-
return (f"{scale}{hk_field['value']}" if hk_field["unit"] in ("yr", "year")
110-
else hk_field["value"])
182+
# in the following cases, the calendar (B or J) is givent by the besselian flag
183+
# We force to use the string representation to avoid breaking unit tests.
184+
elif representation == "mjd":
185+
time = Time(f"{timestamp}", format="mjd")
186+
return (Time(time.byear_str) if besselian else time)
187+
elif representation == "jd":
188+
time = Time(f"{timestamp}", format="jd")
189+
return (Time(time.byear_str) if besselian else time)
190+
elif representation == "iso":
191+
time = Time(f"{timestamp}", format="iso")
192+
return (Time(time.byear_str) if besselian else time)
193+
194+
return None
111195

112-
def _get_space_frame(self, obstime=None):
196+
def _get_space_frame(self):
113197
"""
114198
Build an astropy space frame instance from the MIVOT annotations.
115199
116200
- Equinox are supported for FK4/5
117201
- Reference location is not supported
118202
119-
parameters
120-
----------
121-
obstime: str
122-
Observation time is given to the space frame builder (this method) because
123-
it must be set by the coordinate system constructor in case of FK4 frame.
124203
returns
125204
-------
126205
FK2, FK5, ICRS or Galactic
@@ -133,14 +212,15 @@ def _get_space_frame(self, obstime=None):
133212
if frame == 'fk4':
134213
self._map_coord_names = skycoord_param_default
135214
if "equinox" in coo_sys:
136-
equinox = self._set_year_time_format(coo_sys["equinox"], True)
137-
return FK4(equinox=equinox, obstime=obstime)
215+
equinox = self._get_time_instance(coo_sys["equinox"], True)
216+
# by FK4 takes obstime=equinox by default
217+
return FK4(equinox=equinox)
138218
return FK4()
139219

140220
if frame == 'fk5':
141221
self._map_coord_names = skycoord_param_default
142222
if "equinox" in coo_sys:
143-
equinox = self._set_year_time_format(coo_sys["equinox"])
223+
equinox = self._get_time_instance(coo_sys["equinox"])
144224
return FK5(equinox=equinox)
145225
return FK5()
146226

@@ -153,9 +233,7 @@ def _get_space_frame(self, obstime=None):
153233

154234
def _build_sky_coord_from_mango(self):
155235
"""
156-
Build silently a SkyCoord instance from the ``mango:EpochPosition instance``.
157-
No error is trapped, unconsistencies in the ``mango:EpochPosition`` instance will
158-
raise Astropy errors.
236+
Build a SkyCoord instance from the ``mango:EpochPosition instance``.
159237
160238
- The epoch (obstime) is meant to be given in year.
161239
- ICRS frame is taken by default
@@ -170,26 +248,31 @@ def _build_sky_coord_from_mango(self):
170248
kwargs = {}
171249
kwargs["frame"] = self._get_space_frame()
172250

173-
for key, value in self._map_coord_names.items():
174-
# ignore not set parameters
175-
if key not in self._mivot_instance_dict:
251+
for mango_role, skycoord_field in self._map_coord_names.items():
252+
# ignore not mapped parameters
253+
if mango_role not in self._mivot_instance_dict:
176254
continue
177-
hk_field = self._mivot_instance_dict[key]
178-
# format the observation time (J-year by default)
179-
if value == "obstime":
180-
# obstime must be set into the KK4 frame but not as an input parameter
181-
fobstime = self._set_year_time_format(hk_field)
182-
if isinstance(kwargs["frame"], FK4):
183-
kwargs["frame"] = self._get_space_frame(obstime=fobstime)
255+
hk_field = self._mivot_instance_dict[mango_role]
256+
if mango_role == "obsDate":
257+
besselian = isinstance(kwargs["frame"], FK4)
258+
fobstime = self._get_time_instance(hk_field,
259+
besselian=besselian)
260+
# FK4 class has an obstime attribute which must be set at instanciation time
261+
if besselian:
262+
kwargs["frame"] = FK4(equinox=kwargs["frame"].equinox, obstime=fobstime)
263+
# This is not the case for any other space frames
184264
else:
185-
kwargs[value] = fobstime
186-
# Convert the parallax (mango) into a distance
187-
elif value == "distance":
188-
kwargs[value] = (hk_field["value"]
189-
* u.Unit(hk_field["unit"]).to(u.parsec, equivalencies=u.parallax()))
190-
kwargs[value] = kwargs[value] * u.parsec
191-
elif "unit" in hk_field and hk_field["unit"]:
192-
kwargs[value] = hk_field["value"] * u.Unit(hk_field["unit"])
193-
else:
194-
kwargs[value] = hk_field["value"]
265+
kwargs[skycoord_field] = fobstime
266+
# ignore not set parameters
267+
elif (hk_value := hk_field["value"]) is not None:
268+
# Convert the parallax (mango) into a distance
269+
if skycoord_field == "distance":
270+
kwargs[skycoord_field] = (hk_value
271+
* u.Unit(hk_field["unit"]).to(u.parsec, equivalencies=u.parallax()))
272+
kwargs[skycoord_field] = kwargs[skycoord_field] * u.parsec
273+
elif "unit" in hk_field and hk_field["unit"]:
274+
kwargs[skycoord_field] = hk_value * u.Unit(hk_field["unit"])
275+
else:
276+
kwargs[skycoord_field] = hk_value
277+
195278
return SkyCoord(**kwargs)

pyvo/mivot/tests/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def check_output(self, want, got):
3131
bool
3232
True if the two XML outputs are equal, False otherwise.
3333
"""
34-
return self._format_xml(want.strip()) == self._format_xml(got.strip())
34+
return (self._format_xml(want.strip())
35+
== self._format_xml(got.strip()))
3536

3637
def output_difference(self, want, got):
3738
"""
@@ -121,7 +122,8 @@ def assertXmltreeEqualsFile(xmltree1, xmltree2_file):
121122
The path to the file containing the second XML tree.
122123
"""
123124
xmltree2 = XMLOutputChecker.xmltree_from_file(xmltree2_file).getroot()
124-
xml_str1 = etree.tostring(xmltree1).decode("utf-8")
125-
xml_str2 = etree.tostring(xmltree2).decode("utf-8")
125+
xml_str1 = etree.tostring(xmltree1).decode("utf-8").strip()
126+
xml_str2 = etree.tostring(xmltree2).decode("utf-8").strip()
126127
checker = XMLOutputChecker()
128+
127129
assert checker.check_output(xml_str1, xml_str2), f"XML trees differ:\n{xml_str1}\n---\n{xml_str2}"

0 commit comments

Comments
 (0)