diff --git a/CHANGES.rst b/CHANGES.rst
index dfb8216e..8a574e55 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,10 @@
Enhancements and Fixes
----------------------
+- Support the flat representation of the MANGO observation dates
+ as modeled in the final version of the MANGO data model (DecimalYear,
+ BesselianEpoch, JulianEpoch, mjd, jd, iso) [#726]
+
- Support VOTableFile in accessible_table and broadcast_samp [#745]
- Declaratively define new-style standard IDs in Servicetype constraint [#744]
diff --git a/pyvo/mivot/features/sky_coord_builder.py b/pyvo/mivot/features/sky_coord_builder.py
index c9713c72..1b7a6e7c 100644
--- a/pyvo/mivot/features/sky_coord_builder.py
+++ b/pyvo/mivot/features/sky_coord_builder.py
@@ -19,17 +19,15 @@ class SkyCoordBuilder:
set of required parameters (a position).
- In this implementation, only the mango:EpochPosition class is supported since
it contains the information required to compute the epoch propagation which is a major use-case
+
+
+ parameters
+ -----------
+ mivot_instance: dict or MivotInstance
+ Python object generated from the MIVOT block as either a Pyhon object or a dict
'''
def __init__(self, mivot_instance):
- '''
- Constructor
-
- parameters
- -----------
- mivot_instance: dict or MivotInstance
- Python object generated from the MIVOT block as either a Pyhon object or a dict
- '''
self._mivot_instance_dict = mivot_instance.to_dict()
self._map_coord_names = None
@@ -89,17 +87,12 @@ def _get_time_instance(self, hk_field, besselian=False):
-----
MappingError: if the Time instance cannot be built for some reason
"""
- # Process complex type "mango:DateTime"
- if hk_field['dmtype'] == "mango:DateTime":
- representation = hk_field['representation']['value']
- timestamp = hk_field['dateTime']['value']
- # Process complex type "coords:epoch" used for the space frame equinox
- elif hk_field['dmtype'] == "coords:Epoch":
+ if hk_field['dmtype'] == "coords:Epoch":
representation = 'yr' if "unit" not in hk_field else hk_field.get("unit")
timestamp = hk_field['value']
# Process simple attribute
else:
- representation = hk_field.get("unit")
+ representation = hk_field.get("dmtype")
timestamp = hk_field.get("value")
if not representation or not timestamp:
@@ -113,7 +106,7 @@ def _get_time_instance(self, hk_field, besselian=False):
return time_instance
- def _build_time_instance(self, timestamp, representation, besselian=False):
+ def _build_time_instance(self, timestamp, dmtype, besselian=False):
"""
Build a Time instance matching the input parameters.
- Returns None if the parameters do not allow any Time setup
@@ -122,17 +115,20 @@ def _build_time_instance(self, timestamp, representation, besselian=False):
parameters
----------
timestamp: string or number
- The timestamp must comply with the given representation
- representation: string
- year, iso, ... (See MANGO primitive types derived from ivoa:timeStamp)
+ The timestamp must comply with the given dmtype
+ dmtype: string
+ mango:DecimalYear, mango:JulianEpoch, mango:BesselianEpoch,
+ mango:iso, mango:mjd, mango:jd
+ (See MANGO primitive types derived from ivoa:timeStamp)
besselian: boolean (optional)
Flag telling to use the besselain calendar. We assume it to only be
- relevant for FK5 frame
+ relevant for FK4 frame
returns
-------
Time instance or None
"""
- if representation in ("year", "yr", "y"):
+ # these types are not MANGO dmtype, however they can encountered in some free-style datasets.
+ if dmtype in ("mango:year", "yr", "y"):
# it the timestamp is numeric, we infer its format from the besselian flag
if isinstance(timestamp, numbers.Number):
return Time(f"{('B' if besselian else 'J')}{timestamp}",
@@ -159,9 +155,24 @@ def _build_time_instance(self, timestamp, representation, besselian=False):
return None
# in the following cases, the calendar (B or J) is given by the besselian flag
# We force to use the string representation to avoid breaking unit tests.
- elif representation in ("mjd", "jd", "iso"):
- time = Time(f"{timestamp}", format=representation)
+ elif dmtype in ("mango:mjd", "mango:jd", "mango:iso"):
+ astropyformat = dmtype.split(":")[1]
+ time = Time(f"{timestamp}", format=astropyformat)
return (Time(time.byear_str) if besselian else time)
+ elif dmtype == "mango:DecimalYear":
+ return Time(float(timestamp), format="decimalyear")
+ elif dmtype == "mango:BesselianEpoch":
+ # we accept both "Bxxxx" and "Jxxxx" formats for besselian years,
+ # because the prefix is stripped and this does not impact the result.
+ if isinstance(timestamp, str) and (timestamp.startswith("B") or timestamp.startswith("J")):
+ timestamp = timestamp[1:]
+ return Time(float(timestamp), format="byear")
+ elif dmtype == "mango:JulianEpoch":
+ # we accept both "Bxxxx" and "Jxxxx" formats for julian years,
+ # because the prefix is stripped and this does does not impact the result.
+ if isinstance(timestamp, str) and (timestamp.startswith("B") or timestamp.startswith("J")):
+ timestamp = timestamp[1:]
+ return Time(float(timestamp), format="jyear")
return None
diff --git a/pyvo/mivot/tests/data/reference/mango_object.xml b/pyvo/mivot/tests/data/reference/mango_object.xml
index 8055aa3a..6fcf5cdc 100644
--- a/pyvo/mivot/tests/data/reference/mango_object.xml
+++ b/pyvo/mivot/tests/data/reference/mango_object.xml
@@ -186,10 +186,7 @@
-
-
-
-
+
diff --git a/pyvo/mivot/tests/data/simbad-cone-mivot.xml b/pyvo/mivot/tests/data/simbad-cone-mivot.xml
index b0987713..a2a11658 100644
--- a/pyvo/mivot/tests/data/simbad-cone-mivot.xml
+++ b/pyvo/mivot/tests/data/simbad-cone-mivot.xml
@@ -41,10 +41,7 @@
-
-
-
-
+
diff --git a/pyvo/mivot/tests/test_mango_annoter.py b/pyvo/mivot/tests/test_mango_annoter.py
index 6c5c38c7..58477f49 100644
--- a/pyvo/mivot/tests/test_mango_annoter.py
+++ b/pyvo/mivot/tests/test_mango_annoter.py
@@ -15,6 +15,7 @@
from pyvo.mivot.version_checker import check_astropy_version
from pyvo.mivot.utils.xml_utils import XmlUtils
from pyvo.mivot.writer.instances_from_models import InstancesFromModels
+from pyvo.mivot.utils.exceptions import MappingError
# Enable MIVOT-specific features in the pyvo library
@@ -70,6 +71,17 @@ def add_color(builder):
builder.add_mango_color(filter_ids=filter_ids, mapping=mapping, semantics=semantics)
+@pytest.mark.filterwarnings("ignore:root:::")
+def add_color_without_definition(builder):
+
+ filter_ids = {"high": "GAIA/GAIA3.Grp/AB", "low": "GAIA/GAIA3.Grvs/AB"}
+ mapping = {"value": 8.76,
+ "error": {"class": "PErrorAsym1D", "low": 1, "high": 3}
+ }
+ semantics = {"description": "very nice color", "uri": "vocabulary#term", "label": "term"}
+ builder.add_mango_color(filter_ids=filter_ids, mapping=mapping, semantics=semantics)
+
+
@pytest.mark.filterwarnings("ignore:root:::")
def add_photometry(builder):
photcal_id = "GAIA/GAIA3.Grvs/AB"
@@ -82,13 +94,13 @@ def add_photometry(builder):
builder.add_mango_brightness(photcal_id=photcal_id, mapping=mapping, semantics=semantics)
-def add_epoch_positon(builder):
+def add_epoch_position(builder):
frames = {"spaceSys": {"spaceRefFrame": "ICRS", "refPosition": 'BARYCENTER', "equinox": None},
"timeSys": {"timescale": "TCB", "refPosition": 'BARYCENTER'}}
mapping = {"longitude": "_RAJ2000", "latitude": "_DEJ2000",
"pmLongitude": "pmRA", "pmLatitude": "pmDE",
"parallax": "Plx", "radialVelocity": "RV",
- "obsDate": {"representation": "mjd", "dateTime": 579887.6},
+ "obsDate": {"dmtype": "mango:mjd", "value": 579887.6},
"correlations": {"isCovariance": True,
"longitudeLatitude": "RADEcor",
"latitudePmLongitude": "DEpmRAcor", "latitudePmLatitude": "DEpmDEcor",
@@ -109,6 +121,19 @@ def add_epoch_positon(builder):
builder.add_mango_epoch_position(frames=frames, mapping=mapping, semantics=semantics)
+def add_epoch_position_with_wrong_date(builder):
+ frames = {"spaceSys": {"spaceRefFrame": "ICRS", "refPosition": 'BARYCENTER', "equinox": None},
+ "timeSys": {"timescale": "TCB", "refPosition": 'BARYCENTER'}}
+ mapping = {"longitude": "_RAJ2000", "latitude": "_DEJ2000",
+ "obsDate": {"representation": "mango:mjd", "value": 579887.6},
+ }
+ semantics = {"description": "6 parameters position",
+ "uri": "https://www.ivoa.net/rdf/uat/2024-06-25/uat.html#astronomical-location",
+ "label": "Astronomical location"}
+
+ builder.add_mango_epoch_position(frames=frames, mapping=mapping, semantics=semantics)
+
+
@pytest.mark.usefixtures("mocked_fps_grvs", "mocked_fps_grp")
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_all_properties():
@@ -151,12 +176,19 @@ def test_all_properties():
})
add_color(builder)
add_photometry(builder)
- add_epoch_positon(builder)
+ add_epoch_position(builder)
builder.pack_into_votable(schema_check=False)
assert XmlUtils.strip_xml(builder._annotation.mivot_block) == (
XmlUtils.strip_xml(get_pkg_data_contents("data/reference/mango_object.xml"))
)
+ builder = InstancesFromModels(votable, dmid="DR3Name")
+ with pytest.raises(MappingError):
+ add_epoch_position_with_wrong_date(builder)
+
+ with pytest.raises(MappingError):
+ add_color_without_definition(builder)
+
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_extraction_from_votable_header():
diff --git a/pyvo/mivot/tests/test_sky_coord_builder.py b/pyvo/mivot/tests/test_sky_coord_builder.py
index f9bf98bd..56aecc1e 100644
--- a/pyvo/mivot/tests/test_sky_coord_builder.py
+++ b/pyvo/mivot/tests/test_sky_coord_builder.py
@@ -47,7 +47,7 @@
"ref": "pmDE",
},
"obsDate": {
- "dmtype": "ivoa:RealQuantity",
+ "dmtype": "mango:year",
"value": 1991.25,
"unit": "yr",
"ref": None,
@@ -100,7 +100,7 @@
"ref": "parallax",
},
"obsDate": {
- "dmtype": "ivoa:RealQuantity",
+ "dmtype": "mango:year",
"value": 1991.25,
"unit": "yr",
"ref": None,
@@ -292,13 +292,13 @@ def test_time_representation():
"""
# work with a copy to not alter other test functions
mydict = deepcopy(vizier_equin_dict)
- mydict["obsDate"]["unit"] = "mjd"
+ mydict["obsDate"]["dmtype"] = "mango:mjd"
mivot_instance = MivotInstance(**mydict)
scb = SkyCoordBuilder(mivot_instance)
scoo = scb.build_sky_coord()
assert scoo.obstime.jyear_str == "J1864.331"
- mydict["obsDate"]["unit"] = "jd"
+ mydict["obsDate"]["dmtype"] = "mango:jd"
mydict["obsDate"]["value"] = "2460937.36"
mivot_instance = MivotInstance(**mydict)
scb = SkyCoordBuilder(mivot_instance)
@@ -306,10 +306,52 @@ def test_time_representation():
assert scoo.obstime.jyear_str == "J2025.715"
mydict = deepcopy(vizier_equin_dict)
- mydict["obsDate"]["unit"] = "iso"
- mydict["obsDate"]["dmtype"] = "ivoa:string"
+ mydict["obsDate"]["dmtype"] = "mango:iso"
mydict["obsDate"]["value"] = "2025-05-03"
mivot_instance = MivotInstance(**mydict)
scb = SkyCoordBuilder(mivot_instance)
scoo = scb.build_sky_coord()
assert scoo.obstime.jyear_str == "J2025.335"
+
+ for timestamp in ["2025.0", 2025.0]:
+ mydict = deepcopy(vizier_equin_dict)
+ mydict["obsDate"]["dmtype"] = "mango:DecimalYear"
+ mydict["obsDate"]["value"] = timestamp
+ mivot_instance = MivotInstance(**mydict)
+ scb = SkyCoordBuilder(mivot_instance)
+ scoo = scb.build_sky_coord()
+ assert scoo.obstime.decimalyear == 2025.0
+
+ for timestamp in ["B356", "356", 356.0]:
+ mydict = deepcopy(vizier_equin_dict)
+ mydict["obsDate"]["dmtype"] = "mango:BesselianEpoch"
+ mydict["obsDate"]["value"] = timestamp
+ mivot_instance = MivotInstance(**mydict)
+ scb = SkyCoordBuilder(mivot_instance)
+ scoo = scb.build_sky_coord()
+ assert pytest.approx(scoo.obstime.decimalyear, 0.1) == 356.0
+
+ for timestamp in ["J1899", "1899", 1899]:
+ mydict = deepcopy(vizier_equin_dict)
+ mydict["obsDate"]["dmtype"] = "mango:JulianEpoch"
+ mydict["obsDate"]["value"] = timestamp
+ mivot_instance = MivotInstance(**mydict)
+ scb = SkyCoordBuilder(mivot_instance)
+ scoo = scb.build_sky_coord()
+ assert pytest.approx(scoo.obstime.decimalyear, 0.1) == 1899
+
+ mydict = deepcopy(vizier_equin_dict)
+ mydict["obsDate"]["dmtype"] = "mango:year"
+ mydict["obsDate"]["value"] = "turlutu"
+ mivot_instance = MivotInstance(**mydict)
+ scb = SkyCoordBuilder(mivot_instance)
+ with pytest.raises(MappingError):
+ scb.build_sky_coord()
+
+ mydict = deepcopy(vizier_equin_dict)
+ mydict["obsDate"]["dmtype"] = "turlututu"
+ mydict["obsDate"]["value"] = "turlututu"
+ mivot_instance = MivotInstance(**mydict)
+ scb = SkyCoordBuilder(mivot_instance)
+ with pytest.raises(MappingError):
+ scb.build_sky_coord()
diff --git a/pyvo/mivot/writer/mango_object.py b/pyvo/mivot/writer/mango_object.py
index 1f5b06fe..9c3df041 100644
--- a/pyvo/mivot/writer/mango_object.py
+++ b/pyvo/mivot/writer/mango_object.py
@@ -7,7 +7,7 @@
from pyvo.mivot.utils.mivot_utils import MivotUtils
from pyvo.mivot.writer.instance import MivotInstance
from pyvo.mivot.glossary import (
- IvoaType, ModelPrefix, Roles, CoordSystems)
+ IvoaType, ModelPrefix, Roles)
class Property(MivotInstance):
@@ -127,36 +127,6 @@ def _add_epoch_position_errors(self, **errors):
mapping))
return err_instance
- def _add_epoch_position_epoch(self, **mapping):
- """
- Private method building and returning the observation date (DateTime) of the EpohPosition.
-
- Parameters
- ----------
- mapping: dict(representation, datetime)
- Mapping of the DateTime fields
-
- Returns
- -------
- `Property`
- The EpochPosition observation date instance
- """
- datetime_instance = MivotInstance(dmtype=f"{ModelPrefix.mango}:DateTime",
- dmrole=f"{ModelPrefix.mango}:EpochPosition.obsDate")
-
- representation = mapping.get("representation")
- value = mapping["dateTime"]
- if representation not in CoordSystems.time_formats:
- raise MappingError(f"epoch representation {representation} not supported. "
- f"Take on of {CoordSystems.time_formats}")
- datetime_instance.add_attribute(IvoaType.string,
- f"{ModelPrefix.mango}:DateTime.representation",
- value=MivotUtils.as_literal(representation))
- datetime_instance.add_attribute(IvoaType.datetime,
- f"{ModelPrefix.mango}:DateTime.dateTime",
- value=value)
- return datetime_instance
-
def add_epoch_position(self, space_frame_id, time_frame_id, mapping, semantics):
"""
Add an ``EpochPosition`` instance to the properties of the current ``MangoObject``.
@@ -184,7 +154,11 @@ def add_epoch_position(self, space_frame_id, time_frame_id, mapping, semantics):
MivotUtils.populate_instance(ep_instance, "EpochPosition",
mapping, self._table, IvoaType.RealQuantity)
if "obsDate" in mapping:
- ep_instance.add_instance(self._add_epoch_position_epoch(**mapping["obsDate"]))
+ if "dmtype" not in mapping["obsDate"] or "value" not in mapping["obsDate"]:
+ raise MappingError("obsDate requires both 'dmtype' and 'value' keys")
+ ep_instance.add_attribute(dmtype=mapping["obsDate"]["dmtype"],
+ dmrole="mango:EpochPosition.obsDate",
+ value=mapping["obsDate"]["value"])
if "correlations" in mapping:
ep_instance.add_instance(self._add_epoch_position_correlations(**mapping["correlations"]))
if "errors" in mapping: