From bb12ddd33526397bb7931be68450a06b6ebcfd8f Mon Sep 17 00:00:00 2001 From: lmichel Date: Wed, 4 Feb 2026 09:31:34 +0100 Subject: [PATCH 1/7] Update the observation date modeling that has been changed during the MANGO REC process. This is now a simple attribute instead of a complex object. --- CHANGES.rst | 4 +++- pyvo/mivot/features/sky_coord_builder.py | 14 +++++++++----- pyvo/mivot/tests/data/reference/mango_object.xml | 5 +---- pyvo/mivot/tests/data/simbad-cone-mivot.xml | 5 +---- pyvo/mivot/tests/test_mango_annoter.py | 2 +- pyvo/mivot/tests/test_sky_coord_builder.py | 11 +++++------ pyvo/mivot/writer/mango_object.py | 6 +++++- 7 files changed, 25 insertions(+), 22 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 70cdfb1b..a2eaa178 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ Enhancements and Fixes ---------------------- +- Support the flat representation of the MANGO observation dates (year, mjd, jd, iso) [#725] + - Add DEFAULT_JOB_POLL_TIMEOUT constant [#721] - Pass session through to DatalinkService requests (#715) @@ -12,7 +14,7 @@ Enhancements and Fixes - Provide an API for SoftId-compliant management of the 'User-Agent' header [#719] - + Deprecations and Removals ------------------------- diff --git a/pyvo/mivot/features/sky_coord_builder.py b/pyvo/mivot/features/sky_coord_builder.py index c9713c72..fb467063 100644 --- a/pyvo/mivot/features/sky_coord_builder.py +++ b/pyvo/mivot/features/sky_coord_builder.py @@ -90,16 +90,18 @@ 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" + # (obsolete but kept in place until MANGO is a REC) 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 + # (obsolete but kept in place until MANGO is a REC) elif 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: @@ -124,7 +126,8 @@ def _build_time_instance(self, timestamp, representation, besselian=False): timestamp: string or number The timestamp must comply with the given representation representation: string - year, iso, ... (See MANGO primitive types derived from ivoa:timeStamp) + mango:year, 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 @@ -132,7 +135,7 @@ def _build_time_instance(self, timestamp, representation, besselian=False): ------- Time instance or None """ - if representation in ("year", "yr", "y"): + if representation 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,8 +162,9 @@ 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 representation in ("mango:mjd", "mango:jd", "mango:iso"): + astropyformat = representation.split(":")[1] + time = Time(f"{timestamp}", format=astropyformat) return (Time(time.byear_str) if besselian else time) 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..6da1cffc 100644 --- a/pyvo/mivot/tests/test_mango_annoter.py +++ b/pyvo/mivot/tests/test_mango_annoter.py @@ -88,7 +88,7 @@ def add_epoch_positon(builder): 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", diff --git a/pyvo/mivot/tests/test_sky_coord_builder.py b/pyvo/mivot/tests/test_sky_coord_builder.py index f9bf98bd..a2cc6826 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,8 +306,7 @@ 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) diff --git a/pyvo/mivot/writer/mango_object.py b/pyvo/mivot/writer/mango_object.py index 617cb0ac..6a2bd992 100644 --- a/pyvo/mivot/writer/mango_object.py +++ b/pyvo/mivot/writer/mango_object.py @@ -184,7 +184,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: From 1bfd46080989d03bd3f8cca218c153af59bbd2d4 Mon Sep 17 00:00:00 2001 From: lmichel Date: Wed, 4 Feb 2026 09:43:11 +0100 Subject: [PATCH 2/7] set the correct PR number --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a2eaa178..51a17b73 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,7 @@ Enhancements and Fixes ---------------------- -- Support the flat representation of the MANGO observation dates (year, mjd, jd, iso) [#725] +- Support the flat representation of the MANGO observation dates (year, mjd, jd, iso) [#726] - Add DEFAULT_JOB_POLL_TIMEOUT constant [#721] From 99f12eef84e5f192df489741fcfb032ce329f557 Mon Sep 17 00:00:00 2001 From: lmichel Date: Wed, 4 Feb 2026 13:54:54 +0100 Subject: [PATCH 3/7] test wrong obsDate mapping --- pyvo/mivot/tests/test_mango_annoter.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pyvo/mivot/tests/test_mango_annoter.py b/pyvo/mivot/tests/test_mango_annoter.py index 6da1cffc..334ba990 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 @@ -82,7 +83,7 @@ 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", @@ -109,6 +110,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 +165,16 @@ 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) + @pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") def test_extraction_from_votable_header(): From 021fc4efb15063ee948760e9f96a5dd456ecd6ac Mon Sep 17 00:00:00 2001 From: lmichel Date: Wed, 4 Feb 2026 13:54:54 +0100 Subject: [PATCH 4/7] test wrong color definition mapping --- pyvo/mivot/tests/test_mango_annoter.py | 36 ++++++++++++++++++++++++-- pyvo/mivot/writer/mango_object.py | 32 +---------------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/pyvo/mivot/tests/test_mango_annoter.py b/pyvo/mivot/tests/test_mango_annoter.py index 6da1cffc..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,7 +94,7 @@ 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", @@ -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/writer/mango_object.py b/pyvo/mivot/writer/mango_object.py index 6a2bd992..423ba947 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``. From 87955c47a1af33f30d01d439f3985acb965861d4 Mon Sep 17 00:00:00 2001 From: lmichel Date: Wed, 4 Feb 2026 14:35:21 +0100 Subject: [PATCH 5/7] improve coverage --- pyvo/mivot/features/sky_coord_builder.py | 12 +++-------- pyvo/mivot/tests/test_sky_coord_builder.py | 24 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/pyvo/mivot/features/sky_coord_builder.py b/pyvo/mivot/features/sky_coord_builder.py index fb467063..2fa20308 100644 --- a/pyvo/mivot/features/sky_coord_builder.py +++ b/pyvo/mivot/features/sky_coord_builder.py @@ -89,14 +89,7 @@ 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" - # (obsolete but kept in place until MANGO is a REC) - 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 - # (obsolete but kept in place until MANGO is a REC) - 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 @@ -130,7 +123,7 @@ def _build_time_instance(self, timestamp, representation, besselian=False): (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 @@ -167,6 +160,7 @@ def _build_time_instance(self, timestamp, representation, besselian=False): time = Time(f"{timestamp}", format=astropyformat) return (Time(time.byear_str) if besselian else time) + print("================= 4") return None def _get_space_frame(self): diff --git a/pyvo/mivot/tests/test_sky_coord_builder.py b/pyvo/mivot/tests/test_sky_coord_builder.py index a2cc6826..7687a55c 100644 --- a/pyvo/mivot/tests/test_sky_coord_builder.py +++ b/pyvo/mivot/tests/test_sky_coord_builder.py @@ -312,3 +312,27 @@ def test_time_representation(): scb = SkyCoordBuilder(mivot_instance) scoo = scb.build_sky_coord() assert scoo.obstime.jyear_str == "J2025.335" + + mydict = deepcopy(vizier_equin_dict) + mydict["obsDate"]["dmtype"] = "mango:year" + mydict["obsDate"]["value"] = "B356" + mivot_instance = MivotInstance(**mydict) + scb = SkyCoordBuilder(mivot_instance) + with pytest.raises(MappingError): + scb.build_sky_coord() + + 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() From 3ef6dc33beb05387d2ecf998323fb19c57d38411 Mon Sep 17 00:00:00 2001 From: Laurent MICHEL Date: Fri, 17 Apr 2026 10:20:29 +0200 Subject: [PATCH 6/7] 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) --- CHANGES.rst | 4 ++- pyvo/mivot/features/sky_coord_builder.py | 41 +++++++++++++--------- pyvo/mivot/tests/test_sky_coord_builder.py | 33 +++++++++++++---- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 51a17b73..fe6a5d40 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,9 @@ Enhancements and Fixes ---------------------- -- Support the flat representation of the MANGO observation dates (year, mjd, jd, iso) [#726] +- 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] - Add DEFAULT_JOB_POLL_TIMEOUT constant [#721] diff --git a/pyvo/mivot/features/sky_coord_builder.py b/pyvo/mivot/features/sky_coord_builder.py index 2fa20308..9be0320f 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 @@ -108,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 @@ -117,9 +115,10 @@ def _build_time_instance(self, timestamp, representation, besselian=False): parameters ---------- timestamp: string or number - The timestamp must comply with the given representation - representation: string - mango:year, mango:iso, mango:mjd, mango:jd + 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 @@ -128,7 +127,8 @@ def _build_time_instance(self, timestamp, representation, besselian=False): ------- Time instance or None """ - if representation in ("mango: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}", @@ -155,12 +155,21 @@ 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 ("mango:mjd", "mango:jd", "mango:iso"): - astropyformat = representation.split(":")[1] + 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:BesselianYear": + if isinstance(timestamp, str) and (timestamp.startswith("B") or timestamp.startswith("J")): + timestamp = timestamp[1:] + return Time(float(timestamp), format="byear") + elif dmtype == "mango:JulianYear": + if isinstance(timestamp, str) and (timestamp.startswith("B") or timestamp.startswith("J")): + timestamp = timestamp[1:] + return Time(float(timestamp), format="jyear") - print("================= 4") return None def _get_space_frame(self): diff --git a/pyvo/mivot/tests/test_sky_coord_builder.py b/pyvo/mivot/tests/test_sky_coord_builder.py index 7687a55c..8d03a076 100644 --- a/pyvo/mivot/tests/test_sky_coord_builder.py +++ b/pyvo/mivot/tests/test_sky_coord_builder.py @@ -313,13 +313,32 @@ def test_time_representation(): scoo = scb.build_sky_coord() assert scoo.obstime.jyear_str == "J2025.335" - mydict = deepcopy(vizier_equin_dict) - mydict["obsDate"]["dmtype"] = "mango:year" - mydict["obsDate"]["value"] = "B356" - mivot_instance = MivotInstance(**mydict) - scb = SkyCoordBuilder(mivot_instance) - with pytest.raises(MappingError): - scb.build_sky_coord() + 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:BesselianYear" + 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:JulianYear" + 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" From 79e6ce3ad61f9012826df50ed3e7eae2359fb3ed Mon Sep 17 00:00:00 2001 From: Laurent MICHEL Date: Tue, 28 Apr 2026 09:40:22 +0200 Subject: [PATCH 7/7] correct roles for both Besselian and Julian epochs --- pyvo/mivot/features/sky_coord_builder.py | 8 ++++++-- pyvo/mivot/tests/test_sky_coord_builder.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyvo/mivot/features/sky_coord_builder.py b/pyvo/mivot/features/sky_coord_builder.py index 9be0320f..1b7a6e7c 100644 --- a/pyvo/mivot/features/sky_coord_builder.py +++ b/pyvo/mivot/features/sky_coord_builder.py @@ -161,11 +161,15 @@ def _build_time_instance(self, timestamp, dmtype, besselian=False): return (Time(time.byear_str) if besselian else time) elif dmtype == "mango:DecimalYear": return Time(float(timestamp), format="decimalyear") - elif dmtype == "mango:BesselianYear": + 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:JulianYear": + 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") diff --git a/pyvo/mivot/tests/test_sky_coord_builder.py b/pyvo/mivot/tests/test_sky_coord_builder.py index 8d03a076..56aecc1e 100644 --- a/pyvo/mivot/tests/test_sky_coord_builder.py +++ b/pyvo/mivot/tests/test_sky_coord_builder.py @@ -324,7 +324,7 @@ def test_time_representation(): for timestamp in ["B356", "356", 356.0]: mydict = deepcopy(vizier_equin_dict) - mydict["obsDate"]["dmtype"] = "mango:BesselianYear" + mydict["obsDate"]["dmtype"] = "mango:BesselianEpoch" mydict["obsDate"]["value"] = timestamp mivot_instance = MivotInstance(**mydict) scb = SkyCoordBuilder(mivot_instance) @@ -333,7 +333,7 @@ def test_time_representation(): for timestamp in ["J1899", "1899", 1899]: mydict = deepcopy(vizier_equin_dict) - mydict["obsDate"]["dmtype"] = "mango:JulianYear" + mydict["obsDate"]["dmtype"] = "mango:JulianEpoch" mydict["obsDate"]["value"] = timestamp mivot_instance = MivotInstance(**mydict) scb = SkyCoordBuilder(mivot_instance)