diff --git a/CHANGES.rst b/CHANGES.rst index 9816082d..2ea22ff2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,10 @@ Enhancements and Fixes - Add retry option to AsyncTAPJob.fetch_result for transient failures [#696] +- Upgrade of the ``MivotViewer`` API (``xml_viewer`` module removed, support of + multiple mapped objects per row, partial redesign of the public API) - + Improve the gateway between annotations and ``SkyCoord`` objects - + revamp the viewer documentation. [#698] Deprecations and Removals ------------------------- diff --git a/docs/mivot/example.rst b/docs/mivot/example.rst index d2eb8432..910853df 100644 --- a/docs/mivot/example.rst +++ b/docs/mivot/example.rst @@ -1,8 +1,10 @@ +.. _mivot-examples: + ************************************************************ MIVOT (``pyvo.mivot``): How to use annotated data - Examples ************************************************************ -Photometric properties readout +Photometric Properties Readout ============================== This example is based on VOTables provided by the ``XTapDB`` service. @@ -22,32 +24,32 @@ to tell the server to annotate the queried data. (*Please read the comment inside the code snippet carefully to fully understand the process*) - .. code-block:: python +.. code-block:: python - import pytest - from pyvo.utils import activate_features - from pyvo.dal import TAPService - from pyvo.mivot.utils.xml_utils import XmlUtils - from pyvo.mivot.utils.dict_utils import DictUtils - from pyvo.mivot.viewer.mivot_viewer import MivotViewer + import pytest + from pyvo.utils import activate_features + from pyvo.dal import TAPService + from pyvo.mivot.utils.xml_utils import XmlUtils + from pyvo.mivot.utils.dict_utils import DictUtils + from pyvo.mivot.viewer.mivot_viewer import MivotViewer - # Enable MIVOT-specific features in the pyvo library - activate_features("MIVOT") + # Enable MIVOT-specific features in the pyvo library + activate_features("MIVOT") - service = TAPService('https://xcatdb.unistra.fr/xtapdb') - result = service.run_sync( + service = TAPService('https://xcatdb.unistra.fr/xtapdb') + result = service.run_sync( """ SELECT TOP 5 * FROM "public".mergedentry """, format="application/x-votable+xml;content=mivot" ) - # The MIVOT viewer generates the model view of the data - m_viewer = MivotViewer(result, resolve_ref=True) + # The MIVOT viewer generates the model view of the data + m_viewer = MivotViewer(result, resolve_ref=True) - # Print out the Mivot annotations read out of the VOtable - # This statement is just for a pedagogic purpose (access to a private attribute) - XmlUtils.pretty_print(m_viewer._mapping_block) + # Print out the Mivot annotations read out of the VOtable + # This statement is just for a pedagogic purpose (access to a private attribute) + XmlUtils.pretty_print(m_viewer._mapping_block) In this first step we just queried the service and we built the object that will process the Mivot annotations. @@ -65,46 +67,49 @@ The Mivot block printing output is too long to be listed here. However, the scre At instantiation time, the viewer reads the first data row, which must exist, in order to construct a Python object that reflects the mapped model. - .. code-block:: python +.. code-block:: python - # Build a Python object matching the TEMPLATES content and - # which leaves are set with the values of the first row - mango_object = m_viewer.dm_instance + # Build a Python object matching the TEMPLATES content and + # which leaves are set with the values of the first row + mango_object = m_viewer.dm_instance - # Print out the content of the Python object - # This statement is just for a pedagogic purpose - DictUtils.print_pretty_json(mango_object.to_dict()) + # Print out the content of the Python object + # This statement is just for a pedagogic purpose + DictUtils.print_pretty_json(mango_object.to_dict()) The annotations are consumed by this dynamic Python object which leaves are set with the data of the current row. You can explore the structure of this object by using the printed dictionary or standard object paths as shown below. Now, we can iterate through the table data and retrieve an updated Mivot instance for each row. - .. code-block:: python - - while m_viewer.next_row_view(): - if mango_object.dmtype == "mango:MangoObject": - print(f"Read source {mango_object.identifier.value} {mango_object.dmtype}") - for mango_property in mango_object.propertyDock: - if mango_property.dmtype == "mango:Brightness": - if mango_property.value.value: - mag_value = mango_property.value.value - mag_error = mango_property.error.sigma.value - phot_cal = mango_property.photCal - spectral_location = phot_cal.photometryFilter.spectralLocation - mag_filter = phot_cal.identifier.value - spectral_location = phot_cal.photometryFilter.spectralLocation - mag_wl = spectral_location.value.value - sunit = spectral_location.unitexpression.value - - print(f" flux at {mag_wl} {sunit} (filter {mag_filter}) is {mag_value:.2e} +/- {mag_error:.2e}") - - Read source 4XMM J054329.3-682106 mango:MangoObject +.. code-block:: python + + while m_viewer.next_row_view(): + if mango_object.dmtype == "mango:MangoObject": + print(f"Read source {mango_object.identifier.value} {mango_object.dmtype}") + for mango_property in mango_object.propertyDock: + if mango_property.dmtype == "mango:Brightness": + if mango_property.value.value: + mag_value = mango_property.value.value + mag_error = mango_property.error.sigma.value + phot_cal = mango_property.photCal + spectral_location = phot_cal.photometryFilter.spectralLocation + mag_filter = phot_cal.identifier.value + spectral_location = phot_cal.photometryFilter.spectralLocation + mag_wl = spectral_location.value.value + sunit = spectral_location.unitexpression.value + + print(f" flux at {mag_wl} {sunit} (filter {mag_filter}) is {mag_value:.2e} +/- {mag_error:.2e}") + + +.. code-block:: text + + Read source 4XMM J054329.3-682106 mango:MangoObject flux at 0.35 keV (filter XMM/EPIC/EB1) is 8.35e-14 +/- 3.15e-14 flux at 0.75 keV (filter XMM/EPIC/EB2) is 3.26e-15 +/- 5.45e-15 flux at 6.1 keV (filter XMM/EPIC/EB8) is 8.68e-14 +/- 6.64e-14 - ... - ... + ... + ... The same code can easily be connected with matplotlib to plot SEDs as shown below (code not provided). @@ -113,11 +118,11 @@ The same code can easily be connected with matplotlib to plot SEDs as shown belo :width: 500 :alt: XMM SED -It is to noted that the current table row keeps available through the Mivot viewer. +It is to be noted that the current table row keeps available through the Mivot viewer. - .. code-block:: python +.. code-block:: python - row = m_viewer.table_row + row = m_viewer.table_row .. important:: @@ -129,7 +134,7 @@ It is to noted that the current table row keeps available through the Mivot view The same client code can be reused in many places with many datasets, provided they are annotated. -EpochPosition property readout +EpochPosition Property Readout ============================== This example is based on a VOtable resulting on a Vizier cone search. @@ -148,48 +153,47 @@ which models a full source's astrometry at a given date. In the first step below, we run a standard cone search query by using the standard PyVO API. - .. code-block:: python +.. code-block:: python - import pytest - import astropy.units as u - from astropy.coordinates import SkyCoord - from pyvo.dal.scs import SCSService + import pytest + import astropy.units as u + from astropy.coordinates import SkyCoord + from pyvo.dal.scs import SCSService + from pyvo.utils import activate_features + from pyvo.mivot.viewer.mivot_viewer import MivotViewer + from pyvo.mivot.features.sky_coord_builder import SkyCoordBuilder + from pyvo.mivot.utils.dict_utils import DictUtils - from pyvo.utils import activate_features - from pyvo.mivot.viewer.mivot_viewer import MivotViewer - from pyvo.mivot.features.sky_coord_builder import SkyCoordBuilder - from pyvo.mivot.utils.dict_utils import DictUtils + # Enable MIVOT-specific features in the pyvo library + activate_features("MIVOT") - # Enable MIVOT-specific features in the pyvo library - activate_features("MIVOT") + scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main") - scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main") - - query_result = scs_srv.search( + query_result = scs_srv.search( pos=SkyCoord(ra=52.26708 * u.degree, dec=59.94027 * u.degree, frame='icrs'), radius=0.5) - # The MIVOt viewer generates the model view of the data - m_viewer = MivotViewer(query_result, resolve_ref=True) + # The MIVOt viewer generates the model view of the data + m_viewer = MivotViewer(query_result, resolve_ref=True) Once the query is finished, we can get a reference to the object that will process the Mivot annotations. - .. code-block:: python +.. code-block:: python - # Build a Python object matching the TEMPLATES content and - # which leaves are set with the values of the first row - mango_property = m_viewer.dm_instance + # Build a Python object matching the TEMPLATES content and + # which leaves are set with the values of the first row + mango_property = m_viewer.dm_instance - # Print out the content of the Python object - # This statement is just for a pedagogic purpose - DictUtils.print_pretty_json(mango_property.to_dict()) + # Print out the content of the Python object + # This statement is just for a pedagogic purpose + DictUtils.print_pretty_json(mango_property.to_dict()) The annotations are consumed by this dynamic Python object which leaves are set with the data of the current row. You can explore the structure of this object by using standard object paths or by browsing the dictionary shown below. .. code-block:: json - { + { "dmtype": "mango:EpochPosition", "longitude": { "dmtype": "ivoa:RealQuantity", @@ -234,19 +238,19 @@ You can explore the structure of this object by using standard object paths or b } } } - } + } The reader can transform ``EpochPosition`` instances into ``SkyCoord`` instances. These can then be used for further scientific processing. - .. code-block:: python +.. code-block:: python - while m_viewer.next_row_view(): - if mango_property.dmtype == "mango:EpochPosition": - scb = SkyCoordBuilder(mango_property.to_dict()) - # do whatever process with the SkyCoord object - print(scb.build_sky_coord()) + while m_viewer.next_row_view(): + if mango_property.dmtype == "mango:EpochPosition": + scb = SkyCoordBuilder(mango_property.to_dict()) + # do whatever process with the SkyCoord object + print(scb.build_sky_coord()) .. important:: Similar to the previous example, this code can be used with any VOTable with data mapped to MANGO. @@ -255,5 +259,29 @@ You can explore the structure of this object by using standard object paths or b It avoids the need for users to build SkyCoord objects by hand from VOTable fields, which is never an easy task. +Homework +======== + +Simbad has released (Q3 2025) an annotated version of its Cone Search. +It's a good case to exercise this API. + + +.. code-block:: python + + SERVER = "https://simbad.cds.unistra.fr/cone?" + VERB = 2 + RA = 269.452076* u.degree + DEC = 4.6933649* u.degree + SR = 0.1* u.degree + MAXREC = 100 + + scs_srv = SCSService(SERVER) + + query_result = scs_srv.search( + pos=SkyCoord(ra=RA, dec=DEC, frame='icrs'), radius=SR + verbosity=VERB, + RESPONSEFORMAT="mivot", + MAXREC=MAXREC) + -The next section provides some tips to use the API documented in the :ref:`annoter page `. +*The next section provides some tips to use the API documented in the* :ref:`annoter page `. diff --git a/docs/mivot/viewer.rst b/docs/mivot/viewer.rst index 481a8daf..71463de3 100644 --- a/docs/mivot/viewer.rst +++ b/docs/mivot/viewer.rst @@ -23,66 +23,137 @@ Introduction Using the API ============= + .. attention:: + The module based on XPath queries and allowing to browse the XML + annotations (``viewer.XmlViewer``) has been removed from version 1.8 + Integrated Readout ------------------ The ``ModelViewer`` module manages access to data mapped to a model through dynamically generated objects (``MivotInstance`` class). -The example below shows how a VOTable result of a cone-search query can be parsed and data -mapped to the ``EpochPosition`` class. - - -.. doctest-remote-data:: - >>> import astropy.units as u - >>> from astropy.coordinates import SkyCoord - >>> from pyvo.dal.scs import SCSService - >>> from pyvo.utils.prototype import activate_features - >>> from pyvo.mivot.version_checker import check_astropy_version - >>> from pyvo.mivot.viewer.mivot_viewer import MivotViewer - >>> activate_features("MIVOT") - >>> if check_astropy_version() is False: - ... pytest.skip("MIVOT test skipped because of the astropy version.") - >>> scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main") - >>> m_viewer = MivotViewer( - ... scs_srv.search( - ... pos=SkyCoord(ra=52.26708 * u.degree, dec=59.94027 * u.degree, frame='icrs'), - ... radius=0.05 - ... ), - ... resolve_ref=True - ... ) - >>> mivot_instance = m_viewer.dm_instance - >>> print(mivot_instance.dmtype) - mango:EpochPosition - >>> print(mivot_instance.spaceSys.frame.spaceRefFrame.value) - ICRS - >>> while m_viewer.next_row_view(): - ... print(f"position: {mivot_instance.latitude.value} {mivot_instance.longitude.value}") - position: 59.94033461 52.26722684 - - -In this example, the data readout is totally managed by the ``MivotViewer`` instance. -The ``astropy.io.votable`` API is encapsulated in this module. - -Model leaves (class attributes) are complex types that provide additional information: +These objects can be used as standard Python instances, with the fields representing model elements. +They can also be used as Python dictionaries (provided by the ``to_dict()`` method), with the keys +representing the model elements. + +The code below, based on the processing of a cone-search output, demonstrates both uses. + +The first step is to instanciate a viewer that will provide the API for browsing annotations. +The viewer can be built from a VOTable file path, a parsed VOtable (``VOTableFile`` object), +or a ``DALResults`` instance. + +.. code-block:: python + + import astropy.units as u + from astropy.coordinates import SkyCoord + from pyvo.dal.scs import SCSService + from pyvo.utils.prototype import activate_features + from pyvo.mivot.version_checker import check_astropy_version + from pyvo.mivot.viewer.mivot_viewer import MivotViewer + + activate_features("MIVOT") + if check_astropy_version() is False: + pytest.skip("MIVOT test skipped because of the astropy version.") + + scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main") + m_viewer = MivotViewer( + scs_srv.search( + pos=SkyCoord(ra=52.26708 * u.degree, dec=59.94027 * u.degree, + frame='icrs'), + radius=0.05 + ), + resolve_ref=True + ) + +In this example, the query result is mapped to the ``mango:EpochPosition`` class, +but users do not need to know this in advance, since the API provides tools +to discover the mapped models. + +.. code-block:: python + + if m_viewer.get_models().get("mango"): + print("data is mapped to the MANGO data model") +.. code-block:: text + + data is mapped to the MANGO data model + +We can also check which datamodel classes the data is mapped to. + +.. code-block:: python + + mivot_instances = m_viewer.dm_instances + print(f"data is mapped to {len(mivot_instances)} model class(es)") + mivot_instance = m_viewer.dm_instances[0] + print(f"data is mapped to the {mivot_instance.dmtype} class") +.. code-block:: text + + data is mapped to 1 model clsss(es) + data is mapped to the mango:EpochPosition class + +At this point, we know that the data has been mapped to the ``MANGO`` model, +and that the data rows can be interpreted as instances of the ``mango:EpochPosition``. + +.. code-block:: python + + print(mivot_instance.spaceSys.frame.spaceRefFrame.value) + while m_viewer.next_row_view(): + print(f"position: {mivot_instance.latitude.value} {mivot_instance.longitude.value}") + +.. code-block:: text + + ICRS + position: 59.94033461 52.26722684 + ... + +.. important:: + + Coordinate systems are usually mapped in the GLOBALS MIVOT block. + This allows them to be referenced from any other MIVOT element. + The viewer resolves such references when the constructor flag ``resolve_ref`` is set to ``True``. + In this case the coordinate system instances are copied into their host elements. + +The code below shows how to access GLOBALS instances independently of the mapped data. + +.. code-block:: python + + for globals_instance in globals_instances: + print(globals_instance) + +.. code-block:: json + + { + "dmtype": "coords:SpaceSys", + "dmid": "SpaceFrame_ICRS", + "frame": { + "dmrole": "coords:PhysicalCoordSys.frame", + "dmtype": "coords:SpaceFrame", + "spaceRefFrame": { + "dmtype": "ivoa:string", + "value": "ICRS" + } + } + } + +As you can see from the previous examples, model leaves (class attributes) are complex types. +This is because they contain additional metadata as well as values: - ``value``: attribute value - ``dmtype``: attribute type such as defined in the Mivot annotations - ``unit``: attribute unit such as defined in the Mivot annotations -- ``ref``: identifier of the table column mapped on the attribute The model view on a data row can also be passed as a Python dictionary -using the ``dict`` property of ``MivotInstance``. +using the ``to_dict()`` property of ``MivotInstance``. .. code-block:: python - :caption: Working with a model view as a dictionary - (the JSON layout has been squashed for display purpose) - from pyvo.mivot.utils.dict_utils import DictUtils + from pyvo.mivot.utils.dict_utils import DictUtils mivot_instance = m_viewer.dm_instance - mivot_object_dict = mivot_object.dict - + mivot_object_dict = mivot_object.to_dict() DictUtils.print_pretty_json(mivot_object_dict) + +.. code-block:: json + { "dmtype": "EpochPosition", "longitude": {"value": 359.94372764, "unit": "deg"}, @@ -98,9 +169,11 @@ using the ``dict`` property of ``MivotInstance``. }, } -- It is recommended to use a copy of the - dictionary as it will be rebuilt each time the ``dict`` property is invoked. -- The default representation of ``MivotInstance`` instances is made with a pretty +The ``to_hk_dict()`` method extends the model leaves with the references of the mapped columns. + +- It is recommended to work with deepcopies of the + dictionaries as they are rebuilt each time the ``to_dict()`` property is invoked. +- The Python representation (``__repr__()``) of ``MivotInstance`` instances is made with a pretty string serialization of this dictionary. Per-Row Readout @@ -110,41 +183,52 @@ The annotation schema can also be applied to table rows read outside of the ``Mi with the `astropy.io.votable` API: .. code-block:: python - :caption: Accessing the model view of Astropy table rows votable = parse(path_to_votable) table = votable.resources[0].tables[0] # init the viewer mivot_viewer = MivotViewer(votable, resource_number=0) mivot_object = mivot_viewer.dm_instance - # and feed it with the table row - read = [] + # and feed it with the numpy table row for rec in table.array: + # apply the mapping to current row mivot_object.update(rec) - read.append(mivot_object.longitude.value) - # show that the model retrieve the correct data values + # show that the model retrieve the correct values + # ... or do whatever you want assert rec["RAICRS"] == mivot_object.longitude.value assert rec["DEICRS"] == mivot_object.latitude.value -In this case, it is up to the user to ensure that the read data rows are those mapped by the Mivot annotations. +Mivot/Mango as a Direct Gateway from Data to Astropy SkyCoord +------------------------------------------------------------- -For XML Hackers ---------------- - -The model instances can also be serialized as XML elements that can be parsed with XPath queries. +A simple way to get the most out of annotations is to use them +to directly create Astropy objects, without having to parse the metadata, +whether it comes from the annotation or the VOTable. .. code-block:: python - :caption: Accessing the XML view of the mapped model instances - - with MivotViewer(path_to_votable) as mivot_viewer: - while mivot_viewer.next_row_view(): - xml_view = mivot_viewer.xml_view - # do whatever you want with this XML element -It is to be noted that ``mivot_viewer.xml_view`` is a shortcut -for ``mivot_viewer.xml_view.view`` where ``mivot_viewer.xml_view`` -is is an instance of ``pyvo.mivot.viewer.XmlViewer``. -This object provides many functions facilitating the XML parsing. + m_viewer.rewind() + while m_viewer.next_row_view(): + sky_coord_builder = SkyCoordBuilder(mivot_instance) + sky_coord = sky_coord_builder.build_sky_coord() + print(sky_coord) + +.. code-block:: text + + + +In the above example, we assume that the mapped model can be used as a ``SkyCoord`` precursor. +If this is not the case, an error is raised. + +.. important:: + + In the current implementation, the only functioning gateway connects + ``Mango::EpochPosition`` objects with the ``SkyCoord`` class. In future, + we will implement the same mechanism for any property modelled by Mango, + as well as potentially for other IVOA models. Class Generation in a Nutshell ------------------------------ @@ -173,25 +257,27 @@ identifiers, which have the following structure: ``model:a.b``. - Original ``@dmtype`` are kept as attributes of generated Python objects. - The structure of the ``MivotInstance`` objects can be inferred from the mapped model in 2 different ways: - - 1. From the MIVOT instance property ``MivotInstance.dict`` a shown above. + - 1. From the MIVOT instance property ``MivotInstance.to_dict()`` a shown above. This is a pure Python dictionary but its access can be slow because it is generated on the fly each time the property is invoked. - 2. From the internal class dictionary ``MivotInstance.__dict__`` (see the Python `data model `_). - .. code-block:: python - :caption: Exploring the MivotInstance structure with the internal dictionaries +.. code-block:: python + + mivot_instance = mivot_viewer.dm_instance + + print(mivot_instance.__dict__.keys()) + dict_keys(['dmtype', 'longitude', 'latitude', 'pmLongitude', 'pmLatitude', 'epoch', 'Coordinate_coordSys']) - mivot_instance = mivot_viewer.dm_instance + print(mivot_instance.Coordinate_coordSys.__dict__.keys()) + dict_keys(['dmtype', 'dmid', 'dmrole', 'spaceRefFrame']) - print(mivot_instance.__dict__.keys()) - dict_keys(['dmtype', 'longitude', 'latitude', 'pmLongitude', 'pmLatitude', 'epoch', 'Coordinate_coordSys']) + print(mivot_instance.Coordinate_coordSys.spaceRefFrame.__dict__.keys()) + dict_keys(['dmtype', 'value', 'unit', 'ref']) - print(mivot_instance.Coordinate_coordSys.__dict__.keys()) - dict_keys(['dmtype', 'dmid', 'dmrole', 'spaceRefFrame']) - print(mivot_instance.Coordinate_coordSys.spaceRefFrame.__dict__.keys()) - dict_keys(['dmtype', 'value', 'unit', 'ref']) +*More examples can be found* :ref:`here `. Reference/API ============= diff --git a/pyvo/mivot/features/sky_coord_builder.py b/pyvo/mivot/features/sky_coord_builder.py index 2d039a4c..927c1588 100644 --- a/pyvo/mivot/features/sky_coord_builder.py +++ b/pyvo/mivot/features/sky_coord_builder.py @@ -2,39 +2,13 @@ """ Utility transforming MIVOT annotation into SkyCoord instances """ - +import numbers from astropy.coordinates import SkyCoord from astropy import units as u from astropy.coordinates import ICRS, Galactic, FK4, FK5 -from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError - - -class MangoRoles: - """ - Place holder for the roles (attribute names) of the mango:EpochPosition class - """ - LONGITUDE = "longitude" - LATITUDE = "latitude" - PM_LONGITUDE = "pmLongitude" - PM_LATITUDE = "pmLatitude" - PARALLAX = "parallax" - RADIAL_VELOCITY = "radialVelocity" - EPOCH = "obsDate" - FRAME = "frame" - EQUINOX = "equinox" - PMCOSDELTAPPLIED = "pmCosDeltApplied" - - -# Mapping of the MANGO parameters on the SkyCoord parameters -skycoord_param_default = { - MangoRoles.LONGITUDE: 'ra', MangoRoles.LATITUDE: 'dec', MangoRoles.PARALLAX: 'distance', - MangoRoles.PM_LONGITUDE: 'pm_ra_cosdec', MangoRoles.PM_LATITUDE: 'pm_dec', - MangoRoles.RADIAL_VELOCITY: 'radial_velocity', MangoRoles.EPOCH: 'obstime'} - -skycoord_param_galactic = { - MangoRoles.LONGITUDE: 'l', MangoRoles.LATITUDE: 'b', MangoRoles.PARALLAX: 'distance', - MangoRoles.PM_LONGITUDE: 'pm_l_cosb', MangoRoles.PM_LATITUDE: 'pm_b', - MangoRoles.RADIAL_VELOCITY: 'radial_velocity', MangoRoles.EPOCH: 'obstime'} +from astropy.time.core import Time +from pyvo.mivot.glossary import SkyCoordMapping +from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError, MappingError class SkyCoordBuilder: @@ -47,22 +21,27 @@ class SkyCoordBuilder: it contains the information required to compute the epoch propagation which is a major use-case ''' - def __init__(self, mivot_instance_dict): + def __init__(self, mivot_instance): ''' Constructor parameters ----------- - mivot_instance_dict: viewer.MivotInstance.to_dict() - Internal dictionary of the dynamic Python object generated from the MIVOT block + 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_dict + if isinstance(mivot_instance, dict): + self._mivot_instance_dict = mivot_instance + else: + self._mivot_instance_dict = mivot_instance.to_dict() self._map_coord_names = None def build_sky_coord(self): """ Build a SkyCoord instance from the MivotInstance dictionary. - The operation requires the dictionary to have ``mango:EpochPosition`` as dmtype + The operation requires the dictionary to have ``mango:EpochPosition`` as dmtype. + This instance can be either the root of the dictionary or it can be one + of the Mango properties if the root object is a mango:MangoObject instance This is a public method which could be extended to support other dmtypes. returns @@ -75,15 +54,28 @@ def build_sky_coord(self): NoMatchingDMTypeError if the SkyCoord instance cannot be built. """ - if self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:EpochPosition": + + if self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:MangoObject": + property_dock = self._mivot_instance_dict["propertyDock"] + for mango_property in property_dock: + if mango_property["dmtype"] == "mango:EpochPosition": + self._mivot_instance_dict = mango_property + return self._build_sky_coord_from_mango() + raise NoMatchingDMTypeError( + "No INSTANCE with dmtype='mango:EpochPosition' has been found:" + " in the property dock of the MangoObject, " + "cannot build a SkyCoord from annotations") + + elif self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:EpochPosition": return self._build_sky_coord_from_mango() raise NoMatchingDMTypeError( "No INSTANCE with dmtype='mango:EpochPosition' has been found:" " cannot build a SkyCoord from annotations") - def _set_year_time_format(self, hk_field, besselian=False): + def _get_time_instance(self, hk_field, besselian=False): """ Format a date expressed in year as [scale]year + - Exception possibly risen by Astropy are not caught parameters ---------- @@ -94,33 +86,96 @@ def _set_year_time_format(self, hk_field, besselian=False): returns ------- - string or None - attribute value formatted as [scale]year + Time instance or None + + raise + ----- + MappingError: if the Time instance cannot be built for some reason """ - scale = "J" if not besselian else "B" # Process complex type "mango:DateTime - # only "year" representation are supported yet if hk_field['dmtype'] == "mango:DateTime": representation = hk_field['representation']['value'] timestamp = hk_field['dateTime']['value'] - if representation == "year": - return f"{scale}{timestamp}" - return None - return (f"{scale}{hk_field['value']}" if hk_field["unit"] in ("yr", "year") - else hk_field["value"]) + # Process simple attribute + else: + representation = hk_field.get("unit") + timestamp = hk_field.get("value") + + if not representation or not timestamp: + raise MappingError(f"Cannot interpret field {hk_field} " + f"as a {('besselian' if besselian else 'julian')} timestamp") + + time_instance = self. _build_time_instance(timestamp, representation, besselian) + if not time_instance: + raise MappingError(f"Cannot build a Time instance from {hk_field}") - def _get_space_frame(self, obstime=None): + return time_instance + + def _build_time_instance(self, timestamp, representation, besselian=False): + """ + Build a Time instance matching the input parameters. + - Returns None if the parameters do not allow any Time setup + - Exception possibly risen by Astropy are not caught at this level + + 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) + besselian: boolean (optional) + Flag telling to use the besselain calendar. We assume it to only be + relevant for FK5 frame + returns + ------- + Time instance or None + """ + if representation in ["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}", + format=("byear_str" if besselian else "jyear_str")) + if besselian: + if timestamp.startswith("B"): + return Time(f"{timestamp}", format="byear_str") + elif timestamp.startswith("J"): + # a besselain year cannot be given as "Jxxxx" + return None + elif timestamp.isnumeric(): + # we force the string representation not to break the test assertions + return Time(f"B{timestamp}", format="byear_str") + else: + if timestamp.startswith("J"): + return Time(f"{timestamp}", format="jyear_str") + elif timestamp.startswith("B"): + # a julian year cannot be given as "Bxxxx" + return None + elif timestamp.isnumeric(): + # we force the string representation not to break the test assertions + return Time(f"J{timestamp}", format="jyear_str") + # no case matches + return None + # in the following cases, the calendar (B or J) is givent by the besselian flag + # We force to use the string representation to avoid breaking unit tests. + elif representation == "mjd": + time = Time(f"{timestamp}", format="mjd") + return (Time(time.byear_str) if besselian else time) + elif representation == "jd": + time = Time(f"{timestamp}", format="jd") + return (Time(time.byear_str) if besselian else time) + elif representation == "iso": + time = Time(f"{timestamp}", format="iso") + return (Time(time.byear_str) if besselian else time) + + return None + + def _get_space_frame(self): """ Build an astropy space frame instance from the MIVOT annotations. - Equinox are supported for FK4/5 - Reference location is not supported - parameters - ---------- - obstime: str - Observation time is given to the space frame builder (this method) because - it must be set by the coordinate system constructor in case of FK4 frame. returns ------- FK2, FK5, ICRS or Galactic @@ -131,31 +186,30 @@ def _get_space_frame(self, obstime=None): frame = coo_sys["spaceRefFrame"]["value"].lower() if frame == 'fk4': - self._map_coord_names = skycoord_param_default + self._map_coord_names = SkyCoordMapping.default_params if "equinox" in coo_sys: - equinox = self._set_year_time_format(coo_sys["equinox"], True) - return FK4(equinox=equinox, obstime=obstime) + equinox = self._get_time_instance(coo_sys["equinox"], True) + # by FK4 takes obstime=equinox by default + return FK4(equinox=equinox) return FK4() if frame == 'fk5': - self._map_coord_names = skycoord_param_default + self._map_coord_names = SkyCoordMapping.default_params if "equinox" in coo_sys: - equinox = self._set_year_time_format(coo_sys["equinox"]) + equinox = self._get_time_instance(coo_sys["equinox"]) return FK5(equinox=equinox) return FK5() if frame == 'galactic': - self._map_coord_names = skycoord_param_galactic + self._map_coord_names = SkyCoordMapping.galactic_params return Galactic() - self._map_coord_names = skycoord_param_default + self._map_coord_names = SkyCoordMapping.default_params return ICRS() def _build_sky_coord_from_mango(self): """ - Build silently a SkyCoord instance from the ``mango:EpochPosition instance``. - No error is trapped, unconsistencies in the ``mango:EpochPosition`` instance will - raise Astropy errors. + Build a SkyCoord instance from the ``mango:EpochPosition instance``. - The epoch (obstime) is meant to be given in year. - ICRS frame is taken by default @@ -170,26 +224,29 @@ def _build_sky_coord_from_mango(self): kwargs = {} kwargs["frame"] = self._get_space_frame() - for key, value in self._map_coord_names.items(): - # ignore not set parameters - if key not in self._mivot_instance_dict: + for mango_role, skycoord_field in self._map_coord_names.items(): + # ignore not mapped parameters + if mango_role not in self._mivot_instance_dict: continue - hk_field = self._mivot_instance_dict[key] - # format the observation time (J-year by default) - if value == "obstime": - # obstime must be set into the KK4 frame but not as an input parameter - fobstime = self._set_year_time_format(hk_field) - if isinstance(kwargs["frame"], FK4): - kwargs["frame"] = self._get_space_frame(obstime=fobstime) + hk_field = self._mivot_instance_dict[mango_role] + if mango_role == "obsDate": + besselian = isinstance(kwargs["frame"], FK4) + fobstime = self._get_time_instance(hk_field, + besselian=besselian) + # FK4 class has an obstime attribute which must be set at instanciation time + if besselian: + kwargs["frame"] = FK4(equinox=kwargs["frame"].equinox, obstime=fobstime) + # This is not the case for any other space frames else: - kwargs[value] = fobstime - # Convert the parallax (mango) into a distance - elif value == "distance": - kwargs[value] = (hk_field["value"] - * u.Unit(hk_field["unit"]).to(u.parsec, equivalencies=u.parallax())) - kwargs[value] = kwargs[value] * u.parsec - elif "unit" in hk_field and hk_field["unit"]: - kwargs[value] = hk_field["value"] * u.Unit(hk_field["unit"]) - else: - kwargs[value] = hk_field["value"] + kwargs[skycoord_field] = fobstime + # ignore not set parameters + elif (hk_value := hk_field["value"]) is not None: + # Convert the parallax (mango) into a distance + if skycoord_field == "distance": + kwargs[skycoord_field] = ( + (hk_value * u.Unit(hk_field["unit"])).to(u.parsec, equivalencies=u.parallax())) + elif "unit" in hk_field and hk_field["unit"]: + kwargs[skycoord_field] = hk_value * u.Unit(hk_field["unit"]) + else: + kwargs[skycoord_field] = hk_value return SkyCoord(**kwargs) diff --git a/pyvo/mivot/glossary.py b/pyvo/mivot/glossary.py index 3950dedd..e526332a 100644 --- a/pyvo/mivot/glossary.py +++ b/pyvo/mivot/glossary.py @@ -41,7 +41,8 @@ class Roles: correspond to the last path element of the ``dmroles`` as defined in VODML (VODML-ID) """ - #: Roles of the EpochPosition class that are supported + # Roles of the EpochPosition class that are supported + # Do not change the ordering: it used below to access fields EpochPosition = [ "longitude", "latitude", @@ -191,3 +192,22 @@ class EpochPositionAutoMapping: parallax = ["pos.parallax.trig"] #: first word of UCD-s accepted to map the radial velocity radialVelocity = "spect.dopplerVeloc.opt" + + +class SkyCoordMapping: + """ + Mapping of the MANGO:EpochPositiob parameters to the SkyCoord parameters + """ + default_params = { + Roles.EpochPosition[0]: 'ra', Roles.EpochPosition[1]: 'dec', + Roles.EpochPosition[2]: 'distance', + Roles.EpochPosition[3]: 'radial_velocity', + Roles.EpochPosition[4]: 'pm_ra_cosdec', Roles.EpochPosition[5]: 'pm_dec', + Roles.EpochPosition[6]: 'obstime'} + + galactic_params = { + Roles.EpochPosition[0]: 'l', Roles.EpochPosition[1]: 'b', + Roles.EpochPosition[2]: 'distance', + Roles.EpochPosition[3]: 'radial_velocity', + Roles.EpochPosition[4]: 'pm_l_cosb', Roles.EpochPosition[5]: 'pm_b', + Roles.EpochPosition[6]: 'obstime'} diff --git a/pyvo/mivot/seekers/annotation_seeker.py b/pyvo/mivot/seekers/annotation_seeker.py index 5c0d718a..307843f6 100644 --- a/pyvo/mivot/seekers/annotation_seeker.py +++ b/pyvo/mivot/seekers/annotation_seeker.py @@ -155,9 +155,11 @@ def get_templates_tableref(self): def get_templates(self): """ Return a list of TEMPLATES @tableref. + Returns ------- - list: TEMPLATES tablerefs + [string] + tablerefs of all TEMPLATES elements """ templates_found = [] eset = XPath.x_path(self._xml_block, ".//" + Ele.TEMPLATES) @@ -170,19 +172,26 @@ def get_templates(self): def get_templates_block(self, tableref): """ - Return the TEMPLATES mapping block of the table matching @tableref. - If tableref is None returns all values of templates_blocks. + Return the TEMPLATES mapping block of the table identified @tableref. + If tableref is None or equals to Constant.FIRST_TABLE, return the first TEMPLATES. + Parameters ---------- tableref (str): @tableref of the searched TEMPLATES + Returns ------- - dict: TEMPLATES tablerefs and their mapping blocks {'tableref': mapping_block, ...} + XML element: matching TEMPLATES block or None """ # one table: name forced to DEFAULT or take the first if tableref is None or tableref == Constant.FIRST_TABLE: for _, tmpl in self._templates_blocks.items(): return tmpl + + if tableref not in self._templates_blocks: + raise MivotError( + "No TEMPLATES with tableref=" + tableref) + return self._templates_blocks[tableref] """ @@ -191,6 +200,7 @@ def get_templates_block(self, tableref): def get_instance_dmtypes(self): """ Get @dmtypes of all mapped instances + Returns ------- dict: @dmtypes of all mapped instances {GLOBALS: [], TEMPLATES: {}} diff --git a/pyvo/mivot/tests/__init__.py b/pyvo/mivot/tests/__init__.py index eb1d89d1..37810e4f 100644 --- a/pyvo/mivot/tests/__init__.py +++ b/pyvo/mivot/tests/__init__.py @@ -31,7 +31,8 @@ def check_output(self, want, got): bool True if the two XML outputs are equal, False otherwise. """ - return self._format_xml(want.strip()) == self._format_xml(got.strip()) + return (self._format_xml(want.strip()) + == self._format_xml(got.strip())) def output_difference(self, want, got): """ @@ -121,7 +122,8 @@ def assertXmltreeEqualsFile(xmltree1, xmltree2_file): The path to the file containing the second XML tree. """ xmltree2 = XMLOutputChecker.xmltree_from_file(xmltree2_file).getroot() - xml_str1 = etree.tostring(xmltree1).decode("utf-8") - xml_str2 = etree.tostring(xmltree2).decode("utf-8") + xml_str1 = etree.tostring(xmltree1).decode("utf-8").strip() + xml_str2 = etree.tostring(xmltree2).decode("utf-8").strip() checker = XMLOutputChecker() + assert checker.check_output(xml_str1, xml_str2), f"XML trees differ:\n{xml_str1}\n---\n{xml_str2}" diff --git a/pyvo/mivot/tests/data/reference/annotation_seeker.0.2.xml b/pyvo/mivot/tests/data/reference/annotation_seeker.0.2.xml index 8b60bf29..d7f39109 100644 --- a/pyvo/mivot/tests/data/reference/annotation_seeker.0.2.xml +++ b/pyvo/mivot/tests/data/reference/annotation_seeker.0.2.xml @@ -1,6 +1,6 @@ - + diff --git a/pyvo/mivot/tests/data/reference/globals_models.json b/pyvo/mivot/tests/data/reference/globals_models.json index cfdfbbde..88c97fa4 100644 --- a/pyvo/mivot/tests/data/reference/globals_models.json +++ b/pyvo/mivot/tests/data/reference/globals_models.json @@ -1,11 +1,6 @@ -{ - "COLLECTION": [ - "coords:TimeSys", - "coords:SpaceSys", - "mango:coordinates.PhotometryCoordSys", - "ds:experiment.ObsDataset" - ], - "INSTANCE": [ - "ds:experiment.Target" - ] -} \ No newline at end of file +{"coords": "https://www.ivoa.net/xml/STC/20200908/Coords-v1.0.vo-dml.xml", + "cube": "https://volute.g-vo.org/svn/trunk/projects/dm/Cube/vo-dml/Cube-1.0.vo-dml.xml", + "ds": "https://volute.g-vo.org/svn/trunk/projects/dm/DatasetMetadata/vo-dml/DatasetMetadata-1.0.vo-dml.xml", + "ivoa": "https://www.ivoa.net/xml/VODML/IVOA-v1.vo-dml.xml", + "mango": "file:/Users/sao/Documents/IVOA/GitHub/ivoa-dm-examples/tmp/Mango-v1.0.vo-dml.xml", + "meas": "https://www.ivoa.net/xml/Meas/20200908/Meas-v1.0.vo-dml.xml"} \ No newline at end of file diff --git a/pyvo/mivot/tests/data/reference/templates_models.json b/pyvo/mivot/tests/data/reference/templates_models.json deleted file mode 100644 index 2eb7063e..00000000 --- a/pyvo/mivot/tests/data/reference/templates_models.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "_PKTable": { - "COLLECTION": [], - "INSTANCE": [ - "cube:SparseCube" - ] - }, - "Results": { - "COLLECTION": [], - "INSTANCE": [ - "cube:NDPoint", - "cube:Observable", - "meas:Time", - "coords:MJD", - "cube:Observable", - "meas:GenericMeasure", - "coords:PhysicalCoordinate", - "cube:Observable", - "meas:GenericMeasure", - "coords:PhysicalCoordinate", - "meas:Error", - "meas:Symmetrical" - ] - } -} \ No newline at end of file diff --git a/pyvo/mivot/tests/data/simbad-cone-mivot.xml b/pyvo/mivot/tests/data/simbad-cone-mivot.xml new file mode 100644 index 00000000..e101aa0e --- /dev/null +++ b/pyvo/mivot/tests/data/simbad-cone-mivot.xml @@ -0,0 +1,160 @@ + + + +IVOID of the service specification +Used access protocol and version +Data publisher +HTTP request URL +Query execution date +Publisher email address +Successful query + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Distance (in degrees) between this object and the cone-search's target. + + +Main identifier for an object + + + +Right ascension + + +Declination + + +Object type + + + + +Coordinate error major axis + + +Coordinate error minor axis + + +Coordinate error angle + + +Proper motion in RA + + +Proper motion in DEC + + +Proper motion error major axis + + +Proper motion error minor axis + + +Proper motion error angle + + +Parallax + + +Radial Velocity + + +Angular size major axis + + +Angular size minor axis + + +Galaxy ellipse angle + + +MK spectral type + + +Morphological type + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1.0409523503457668E-5NAME Barnard's Star c269.452076958619004.69336496657667Planet 17 57 48.4984700683 +04 41 36.1138796750.02620.029090-801.55110362.3940.0320.03690546.97591
+Truncated result +
+
diff --git a/pyvo/mivot/tests/data/test.instance_multiple.xml b/pyvo/mivot/tests/data/test.instance_multiple.xml new file mode 100644 index 00000000..de0349e6 --- /dev/null +++ b/pyvo/mivot/tests/data/test.instance_multiple.xml @@ -0,0 +1,186 @@ + + + + + + + + + Automatically annotated by XTAPDB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + the flux of the energy band number 1 of the ep camera in sc + + + the flux of the energy band number 2 of the ep camera in sc + + + the flux of the energy band number 3 of the ep camera in sc + + + + + + + + + + + + + + + +
0.00.10.2
1.02.13.2
+
+
diff --git a/pyvo/mivot/tests/data/test.mivot_viewer.first_instance.xml b/pyvo/mivot/tests/data/test.mivot_viewer.first_instance.xml index 2e90f7f4..406af0e9 100644 --- a/pyvo/mivot/tests/data/test.mivot_viewer.first_instance.xml +++ b/pyvo/mivot/tests/data/test.mivot_viewer.first_instance.xml @@ -12,20 +12,11 @@ - - + - - - - - - - - - + diff --git a/pyvo/mivot/tests/test_annotation_seeker.py b/pyvo/mivot/tests/test_annotation_seeker.py index fd741eed..26d2a6d1 100644 --- a/pyvo/mivot/tests/test_annotation_seeker.py +++ b/pyvo/mivot/tests/test_annotation_seeker.py @@ -12,7 +12,7 @@ from pyvo.mivot.utils.dict_utils import DictUtils from pyvo.mivot.version_checker import check_astropy_version from pyvo.mivot.viewer import MivotViewer -from . import XMLOutputChecker +from pyvo.mivot.tests import XMLOutputChecker @pytest.fixture @@ -24,6 +24,14 @@ def a_seeker(): return AnnotationSeeker(m_viewer._mapping_block) +@pytest.fixture +def a_multiple_seeker(): + m_viewer = MivotViewer( + get_pkg_data_filename("data/test.instance_multiple.xml"), + tableref="Results") + return AnnotationSeeker(m_viewer._mapping_block) + + @pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") def test_multiple_templates(): """ @@ -87,3 +95,9 @@ def test_all_reverts(a_seeker): assert a_seeker.get_globals_instance_from_collection( "_CoordinateSystems", "ICRS").get("dmtype") == "coords:SpaceSys" assert a_seeker.get_globals_instance_from_collection("wrong_dmid", "ICRS") is None + + +@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") +def test_multiple_seeker(a_multiple_seeker): + assert (a_multiple_seeker.get_instance_dmtypes()["TEMPLATES"] + == {"Results": ["mango:Brightness", "mango:Brightness", "mango:Brightness"]}) diff --git a/pyvo/mivot/tests/test_mango_annoter.py b/pyvo/mivot/tests/test_mango_annoter.py index 79d4c4cf..6c5c38c7 100644 --- a/pyvo/mivot/tests/test_mango_annoter.py +++ b/pyvo/mivot/tests/test_mango_annoter.py @@ -152,8 +152,7 @@ def test_all_properties(): add_color(builder) add_photometry(builder) add_epoch_positon(builder) - builder.pack_into_votable() - XmlUtils.pretty_print(builder._annotation.mivot_block) + 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")) ) @@ -171,7 +170,7 @@ def test_extraction_from_votable_header(): builder.extract_data_origin() epoch_position_mapping = builder.extract_epoch_position_parameters() builder.add_mango_epoch_position(**epoch_position_mapping) - builder.pack_into_votable() + builder.pack_into_votable(schema_check=True) assert XmlUtils.strip_xml(builder._annotation.mivot_block) == ( XmlUtils.strip_xml(get_pkg_data_contents("data/reference/test_header_extraction.xml")) ) diff --git a/pyvo/mivot/tests/test_mivot_instance.py b/pyvo/mivot/tests/test_mivot_instance.py index 3cc3035b..e3f89438 100644 --- a/pyvo/mivot/tests/test_mivot_instance.py +++ b/pyvo/mivot/tests/test_mivot_instance.py @@ -4,14 +4,9 @@ @author: michel ''' -import os import pytest from astropy.table import Table -from astropy.utils.data import get_pkg_data_filename -from pyvo.mivot.version_checker import check_astropy_version from pyvo.mivot.viewer.mivot_instance import MivotInstance -from pyvo.mivot.utils.mivot_utils import MivotUtils -from pyvo.mivot.viewer import MivotViewer fake_hk_dict = { "dmtype": "EpochPosition", @@ -103,22 +98,6 @@ } -@pytest.fixture -def m_viewer(): - data_path = get_pkg_data_filename(os.path.join("data", - "test.mivot_viewer.xml") - ) - return MivotViewer(data_path, tableref="Results") - - -@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") -def test_xml_viewer(m_viewer): - - xml_instance = m_viewer.xml_viewer.view - dm_instance = MivotInstance(**MivotUtils.xml_to_dict(xml_instance)) - assert dm_instance.to_dict() == test_dict - - def test_mivot_instance_constructor(): """Test the class generation from a dict.""" mivot_object = MivotInstance(**fake_hk_dict) diff --git a/pyvo/mivot/tests/test_mivot_instance_generation.py b/pyvo/mivot/tests/test_mivot_instance_generation.py deleted file mode 100644 index 847005cf..00000000 --- a/pyvo/mivot/tests/test_mivot_instance_generation.py +++ /dev/null @@ -1,183 +0,0 @@ -# Licensed under a 3-clause BSD style license - see LICENSE.rst -""" -Test for mivot.viewer.model_viewer_level3.py and mivot.viewer.mivot_time.py -""" -import os -import pytest -from urllib.request import urlretrieve -from pyvo.mivot.version_checker import check_astropy_version -from pyvo.mivot.viewer import MivotViewer -from pyvo.mivot.utils.mivot_utils import MivotUtils - - -@pytest.mark.remote_data -@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") -def test_model_viewer3(votable_test, simple_votable): - """ - Recursively compare an XML element with an element of MIVOT - class with the function recursive_xml_check. - This test run on 2 votables : votable_test and simple_votable. - """ - m_viewer_simple_votable = MivotViewer(votable_path=simple_votable) - MivotInstance = m_viewer_simple_votable.dm_instance - xml_simple_votable = m_viewer_simple_votable.xml_view - assert xml_simple_votable.tag == 'TEMPLATES' - recusive_xml_check(xml_simple_votable, MivotInstance) - m_viewer_votable_test = MivotViewer(votable_path=votable_test) - m_viewer_votable_test.next_row_view() - mivot_instance = m_viewer_votable_test.dm_instance - xml_votable_test = m_viewer_votable_test.xml_view - assert xml_simple_votable.tag == 'TEMPLATES' - recusive_xml_check(xml_votable_test, mivot_instance) - - -@pytest.mark.remote_data -@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") -def recusive_xml_check(xml_simple_votable, MivotInstance): - if xml_simple_votable.tag == 'TEMPLATES': - recusive_xml_check(xml_simple_votable[0], MivotInstance) - else: - for child in xml_simple_votable: - if child.tag == 'INSTANCE': - for key, value in child.attrib.items(): - if key == 'dmrole': - if value == '': - if child.tag == 'ATTRIBUTE': - recusive_xml_check(child, - getattr(MivotInstance, - MivotInstance._remove_model_name( - child.get('dmrole')))) - elif child.tag == 'INSTANCE': - recusive_xml_check(child, getattr(MivotInstance, - MivotInstance._remove_model_name - (child.get('dmrole')))) - else: - if child.tag == 'ATTRIBUTE': - recusive_xml_check(child, getattr(MivotInstance, - MivotInstance._remove_model_name( - child.get('dmrole')))) - elif child.tag == 'INSTANCE': - recusive_xml_check(child, getattr(MivotInstance, - MivotInstance._remove_model_name( - child.get('dmrole')))) - elif child.tag == 'COLLECTION': - recusive_xml_check(child, getattr(MivotInstance, - MivotInstance._remove_model_name( - child.get('dmrole')))) - elif child.tag == 'COLLECTION': - for key, value in child.attrib.items(): - assert len(getattr(MivotInstance, - MivotInstance._remove_model_name(child.get('dmrole')))) == len(child) - i = 0 - for child2 in child: - recusive_xml_check(child2, getattr(MivotInstance, MivotInstance._remove_model_name - (child.get('dmrole')))[i]) - i += 1 - elif child.tag == 'ATTRIBUTE': - MivotInstance_attribute = getattr(MivotInstance, - MivotInstance._remove_model_name(child.get('dmrole'))) - for key, value in child.attrib.items(): - if key == 'dmtype': - assert MivotInstance_attribute.dmtype in value - elif key == 'value': - if (MivotInstance_attribute.value is not None - and not isinstance(MivotInstance_attribute.value, bool)): - if isinstance(MivotInstance_attribute.value, float): - pytest.approx(float(value), MivotInstance_attribute.value, 0.0001) - else: - assert value == MivotInstance_attribute.value - elif child.tag.startswith("REFERENCE"): - # Viewer not in resolve_ref mode: REFRENCEs are not filtered - pass - else: - print(child.tag) - assert False - - -@pytest.mark.remote_data -@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") -def test_dict_model_viewer3(votable_test, simple_votable): - """ - To test the generation of the MIVOT class, the function builds a ModelViewerLevel3 - with his MIVOT class and his previous dictionary from XML. - Then, it calls the function recursive_check which recursively compares an element of MIVOT class - with the dictionary on which it was built. - MIVOT class is itself a dictionary with only essential information of the ModelViewerLevel3._dict. - This test run on 2 votables : votable_test and simple_votable. - """ - m_viewer_votable_test = MivotViewer(votable_path=votable_test) - m_viewer_votable_test.next_row_view() - mivot_instance = m_viewer_votable_test.dm_instance - _dict = MivotUtils.xml_to_dict(m_viewer_votable_test.xml_viewer.view) - recursive_check(mivot_instance, **_dict) - mivot_instance = m_viewer_votable_test.dm_instance - _dict = MivotUtils.xml_to_dict(m_viewer_votable_test.xml_view) - recursive_check(mivot_instance, **_dict) - - -def recursive_check(MivotInstance, **kwargs): - for key, value in kwargs.items(): - # the root instance ha no role: this makes an empty value in the unpacked dict - if key == '': - continue - if isinstance(value, list): - nbr_item = 0 - for item in value: - if isinstance(item, dict): - assert 'dmtype' in item.keys() - recursive_check(getattr(MivotInstance, - MivotInstance._remove_model_name(key))[nbr_item], - **item - ) - nbr_item += 1 - elif isinstance(value, dict) and 'value' not in value: - # for INSTANCE of INSTANCEs dmrole needs model_name - assert MivotInstance._remove_model_name(key, True) in vars(MivotInstance).keys() - recursive_check(getattr(MivotInstance, MivotInstance._remove_model_name(key, True)), **value) - else: - if isinstance(value, dict) and MivotInstance._is_leaf(**value): - assert value.keys().__contains__('dmtype' and 'value' and 'unit' and 'ref') - lower_dmtype = value['dmtype'].lower() - if "real" in lower_dmtype or "double" in lower_dmtype or "float" in lower_dmtype: - assert isinstance(value['value'], float) - elif "bool" in lower_dmtype: - assert isinstance(value['value'], bool) - elif value['dmtype'] is None: - assert (value['value'] in - ('notset', 'noset', 'null', 'none', 'NotSet', 'NoSet', 'Null', 'None')) - else: - if value['value'] is not None: - assert isinstance(value['value'], str) - recursive_check(getattr(MivotInstance, MivotInstance._remove_model_name(key)), **value) - else: - assert key == 'dmtype' or 'value' - - -@pytest.fixture -def votable_test(data_path, data_sample_url): - votable_name = "vizier_csc2_gal.annot.xml" - votable_path = os.path.join(data_path, "data", votable_name) - urlretrieve(data_sample_url + votable_name, - votable_path) - yield votable_path - os.remove(votable_path) - - -@pytest.fixture -def simple_votable(data_path, data_sample_url): - votable_name = "simple-annotation-votable.xml" - votable_path = os.path.join(data_path, "data", votable_name) - urlretrieve(data_sample_url + votable_name, - votable_path) - yield votable_path - os.remove(votable_path) - - -@pytest.fixture -def data_path(): - return os.path.dirname(os.path.realpath(__file__)) - - -@pytest.fixture -def data_sample_url(): - return "https://raw.githubusercontent.com/ivoa/dm-usecases/main/pyvo-ci-sample/" diff --git a/pyvo/mivot/tests/test_mivot_viewer.py b/pyvo/mivot/tests/test_mivot_viewer.py index ff79da42..01aad9ac 100644 --- a/pyvo/mivot/tests/test_mivot_viewer.py +++ b/pyvo/mivot/tests/test_mivot_viewer.py @@ -13,6 +13,153 @@ from pyvo.mivot.viewer import MivotViewer from astropy import version as astropy_version +dm_raw_instances = [ + { + "dmrole": "", + "dmtype": "mango:Brightness", + "value": { + "dmtype": "ivoa:RealQuantity", + "value": None, + "unit": None, + "ref": "SC_EP_1_FLUX", + }, + }, + { + "dmrole": "", + "dmtype": "mango:Brightness", + "value": { + "dmtype": "ivoa:RealQuantity", + "value": None, + "unit": None, + "ref": "SC_EP_2_FLUX", + }, + }, + { + "dmrole": "", + "dmtype": "mango:Brightness", + "value": { + "dmtype": "ivoa:RealQuantity", + "value": None, + "unit": None, + "ref": "SC_EP_3_FLUX", + }, + }, +] + +globals_photcal = { + "dmid": "CoordSystem_XMM_EB1_id", + "dmtype": "Phot:PhotCal", + "identifier": { + "dmtype": "ivoa:string", + "value": "XMM/EPIC/EB1", + "unit": None, + "ref": None, + }, + "magnitudeSystem": { + "dmrole": "Phot:PhotCal.magnitudeSystem", + "dmtype": "Phot:MagnitudeSystem", + "type": { + "dmtype": "Phot:TypeOfMagSystem", + "value": "XMM", + "unit": None, + "ref": None, + }, + "referenceSpectrum": { + "dmtype": "ivoa:anyURI", + "value": "https://xmm-tools.cosmos.esa.int/external" + "/xmm_user_support/documentation/sas_usg/USG/SASUSG.html", + "unit": None, + "ref": None, + }, + }, + "photometryFilter": { + "dmid": "CoordSystem_XMM_FILTER_EB1_id", + "dmtype": "Phot:PhotometryFilter", + "dmrole": "Phot:PhotCal.photometryFilter", + "identifier": { + "dmtype": "ivoa:string", + "value": "XMM/EPIC/EB1", + "unit": None, + "ref": None, + }, + "name": { + "dmtype": "ivoa:string", + "value": "XMM EPIC EB1", + "unit": None, + "ref": None, + }, + "description": { + "dmtype": "ivoa:string", + "value": "Soft", + "unit": None, + "ref": None, + }, + "bandName": { + "dmtype": "ivoa:string", + "value": "EB1", + "unit": None, + "ref": None, + }, + "spectralLocation": { + "dmrole": "Phot:PhotometryFilter.spectralLocation", + "dmtype": "Phot:SpectralLocation", + "ucd": { + "dmtype": "Phot:UCD", + "value": "em.wl.effective", + "unit": None, + "ref": None, + }, + "unitexpression": { + "dmtype": "ivoa:Unit", + "value": "keV", + "unit": None, + "ref": None, + }, + "value": {"dmtype": "ivoa:real", "value": 0.35, "unit": None, "ref": None}, + }, + "bandwidth": { + "dmrole": "Phot:PhotometryFilter.bandwidth", + "dmtype": "Phot:Bandwidth", + "ucd": { + "dmtype": "Phot:UCD", + "value": "instr.bandwidth;stat.fwhm", + "unit": None, + "ref": None, + }, + "unitexpression": { + "dmtype": "ivoa:Unit", + "value": "keV", + "unit": None, + "ref": None, + }, + "extent": {"dmtype": "ivoa:real", "value": 0.3, "unit": None, "ref": None}, + "start": {"dmtype": "ivoa:real", "value": 0.2, "unit": None, "ref": None}, + "stop": {"dmtype": "ivoa:real", "value": 0.5, "unit": None, "ref": None}, + }, + "transmissionCurve": { + "dmrole": "Phot:PhotometryFilter.transmissionCurve", + "dmtype": "Phot:TransmissionCurve", + "access": { + "dmrole": "Phot:TransmissionCurve.access", + "dmtype": "Phot:Access", + "reference": { + "dmtype": "ivoa:anyURI", + "value": "https://xmm-tools.cosmos.esa.int/external/xmm_user_support" + "/documentation/sas_usg/USG/SASUSG.html", + "unit": None, + "ref": None, + }, + "format": { + "dmtype": "ivoa:string", + "value": "text/html", + "unit": None, + "ref": None, + }, + }, + }, + }, +} + @pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") def test_get_first_instance_dmtype(path_to_first_instance): @@ -21,12 +168,14 @@ def test_get_first_instance_dmtype(path_to_first_instance): used to find the first INSTANCE/COLLECTION in TEMPLATES. """ m_viewer = MivotViewer(votable_path=path_to_first_instance) - assert m_viewer.get_first_instance_dmtype("one_instance") == "one_instance" - assert m_viewer.get_first_instance_dmtype("coll_and_instances") == "first" - assert m_viewer.get_first_instance_dmtype("one_collection") == Constant.ROOT_COLLECTION - assert m_viewer.get_first_instance_dmtype("only_collection") == Constant.ROOT_COLLECTION - with pytest.raises(Exception, match="Can't find the first INSTANCE/COLLECTION in TEMPLATES"): - m_viewer.get_first_instance_dmtype("empty") + assert m_viewer.get_dm_instance_dmtypes("one_instance")[0] == "one_instance" + assert m_viewer.get_dm_instance_dmtypes("some_instances")[0] == "first" + + with pytest.raises(Exception, match="Can't find INSTANCE in TEMPLATES"): + m_viewer.get_dm_instance_dmtypes("empty") + + with pytest.raises(Exception, match="No TEMPLATES with tableref=not_existing_tableref"): + m_viewer.get_dm_instance_dmtypes("not_existing_tableref") @pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") @@ -57,10 +206,9 @@ def test_global_getters(m_viewer): Test each getter for TEMPLATES of the model_viewer. """ assert m_viewer.get_table_ids() == ['_PKTable', 'Results'] - assert m_viewer.get_globals_models() == DictUtils.read_dict_from_file( + assert m_viewer.get_models() == DictUtils.read_dict_from_file( get_pkg_data_filename("data/reference/globals_models.json")) - assert m_viewer.get_templates_models() == DictUtils.read_dict_from_file( - get_pkg_data_filename("data/reference/templates_models.json")) + m_viewer._connect_table('_PKTable') row = m_viewer.next_table_row() assert row[0] == '5813181197970338560' @@ -77,13 +225,12 @@ def test_global_getters(m_viewer): @pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") def test_no_mivot(path_no_mivot): """ - Test each getter for GLOBALS of the model_viewer specific . + Test the viewer behavior when there is no mapping """ m_viewer = MivotViewer(path_no_mivot) assert m_viewer.get_table_ids() is None - assert m_viewer.get_globals_models() is None + assert m_viewer.get_models() is None - assert m_viewer.get_templates_models() is None with pytest.raises(MappingError): m_viewer._connect_table('_PKTable') with pytest.raises(MappingError): @@ -92,6 +239,57 @@ def test_no_mivot(path_no_mivot): assert m_viewer.next_table_row() is None +@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") +def test_instance_mutiple_in_templates(path_to_multiple_instance): + """ + Test case with a TEMPLATES containing multiple instances + """ + m_viewer = MivotViewer(votable_path=path_to_multiple_instance) + instance_dict = [] + # test the DM instances children of TEMPLATES before their values are set + for dmi in m_viewer.dm_instances: + instance_dict.append(dmi.to_hk_dict()) + assert instance_dict == dm_raw_instances + + # test the DM instances children of TEMPLATES set with the values of the first row + m_viewer.next_row_view() + row_values = [] + for dmi in m_viewer.dm_instances: + row_values.append(dmi.value.value) + assert row_values == pytest.approx([0.0, 0.1, 0.2], rel=1e-3) + + # test the DM instances children of TEMPLATES set with the values of the second row + m_viewer.next_row_view() + row_values = [] + for dmi in m_viewer.dm_instances: + row_values.append(dmi.value.value) + assert row_values == pytest.approx([1.0, 2.1, 3.2], rel=1e-3) + + +@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") +def test_globals_instances(path_to_multiple_instance): + """ + Test case for the GLOBALS instance access as MivotInstances + """ + m_viewer = MivotViewer(votable_path=path_to_multiple_instance) + instance_dict = [] + photcals = 0 + photfilters = 0 + # test the DM instances children of TEMPLATES before their values are set + for dmi in m_viewer.dm_globals_instances: + if dmi.dmtype == "Phot:PhotCal": + photcals += 1 + elif dmi.dmtype == "Phot:PhotometryFilter": + photfilters += 1 + else: + assert False, f"Unexpected dmtype {dmi.dmtype} in GLOBALS " + instance_dict.append(dmi.to_hk_dict()) + assert photcals == 3 + assert photfilters == 3 + # just check the first one + assert instance_dict[0] == globals_photcal + + def test_check_version(path_to_viewer): if not check_astropy_version(): with pytest.raises(Exception, @@ -125,8 +323,16 @@ def path_to_viewer(): return get_pkg_data_filename(os.path.join("data", votable_name)) +@pytest.fixture +def path_to_multiple_instance(): + + votable_name = "test.instance_multiple.xml" + return get_pkg_data_filename(os.path.join("data", votable_name)) + + @pytest.fixture def path_to_first_instance(): + votable_name = "test.mivot_viewer.first_instance.xml" return get_pkg_data_filename(os.path.join("data", votable_name)) diff --git a/pyvo/mivot/tests/test_sky_coord_builder.py b/pyvo/mivot/tests/test_sky_coord_builder.py index b38062a4..21877550 100644 --- a/pyvo/mivot/tests/test_sky_coord_builder.py +++ b/pyvo/mivot/tests/test_sky_coord_builder.py @@ -7,10 +7,17 @@ for the output of this service. ''' import pytest +from copy import deepcopy +from astropy.utils.data import get_pkg_data_filename from pyvo.mivot.version_checker import check_astropy_version from pyvo.mivot.viewer.mivot_instance import MivotInstance from pyvo.mivot.features.sky_coord_builder import SkyCoordBuilder from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError +from pyvo.mivot.viewer.mivot_viewer import MivotViewer +from pyvo.utils import activate_features + +# Enable MIVOT-specific features in the pyvo library +activate_features("MIVOT") # annotations generated by Vizier as given to the MivotInstance vizier_dict = { @@ -218,7 +225,7 @@ def test_vizier_output(): @pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") def test_vizier_output_with_equinox_and_parallax(): - """Test the SkyCoord issued from the modofier Vizier response * + """Test the SkyCoord issued from the modified Vizier response * (parallax added and FK5 + Equinox frame) """ mivot_instance = MivotInstance(**vizier_equin_dict) @@ -226,13 +233,56 @@ def test_vizier_output_with_equinox_and_parallax(): scoo = scb.build_sky_coord() assert (str(scoo).replace("\n", "").replace(" ", "") == "") - vizier_equin_dict["spaceSys"]["frame"]["spaceRefFrame"]["value"] = "FK4" - mivot_instance = MivotInstance(**vizier_equin_dict) + mydict = deepcopy(vizier_equin_dict) + mydict["spaceSys"]["frame"]["spaceRefFrame"]["value"] = "FK4" + mivot_instance = MivotInstance(**mydict) scoo = mivot_instance.get_SkyCoord() assert (str(scoo).replace("\n", "").replace(" ", "") - == "") + + +@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") +def test_simad_cs_output(): + """Test the SkyCoord issued from a Simbad SCS response + """ + filename = get_pkg_data_filename('data/simbad-cone-mivot.xml') + m_viewer = MivotViewer(filename, resolve_ref=True) + mivot_instance = m_viewer.dm_instance + scb = SkyCoordBuilder(mivot_instance.to_dict()) + scoo = scb.build_sky_coord() + + assert (str(scoo).replace("\n", "").replace(" ", "") + == "") + assert str(scoo.obstime) == "J2000.000" + + +def test_time_representation(): + """ + Test various time representations + Inconsistent values are not tested since there are detected by ``astropy.core.Time`` + """ + # work with a copy to not alter other test functions + mydict = deepcopy(vizier_equin_dict) + mydict["obsDate"]["unit"] = "mjd" + scb = SkyCoordBuilder(mydict) + scoo = scb.build_sky_coord() + assert scoo.obstime.jyear_str == "J1864.331" + + mydict["obsDate"]["unit"] = "jd" + mydict["obsDate"]["value"] = "2460937.36" + scb = SkyCoordBuilder(mydict) + scoo = scb.build_sky_coord() + assert scoo.obstime.jyear_str == "J2025.715" + + mydict["obsDate"]["unit"] = "iso" + mydict["obsDate"]["value"] = "2025-05-03" + scoo = scb.build_sky_coord() + scb = SkyCoordBuilder(mydict) + assert scoo.obstime.jyear_str == "J2025.335" diff --git a/pyvo/mivot/tests/test_user_api.py b/pyvo/mivot/tests/test_user_api.py index 44b010b4..39de509f 100644 --- a/pyvo/mivot/tests/test_user_api.py +++ b/pyvo/mivot/tests/test_user_api.py @@ -15,7 +15,6 @@ from pyvo.mivot.viewer import MivotViewer from astropy.io.votable import parse - ref_ra = [ 0.04827189, 0.16283175, diff --git a/pyvo/mivot/tests/test_xml_viewer.py b/pyvo/mivot/tests/test_xml_viewer.py deleted file mode 100644 index 1a0cd713..00000000 --- a/pyvo/mivot/tests/test_xml_viewer.py +++ /dev/null @@ -1,67 +0,0 @@ -# Licensed under a 3-clause BSD style license - see LICENSE.rst -""" -Test for mivot.viewer.model_viewer_level2.py -""" -import pytest -try: - from defusedxml.ElementTree import Element as element -except ImportError: - from xml.etree.ElementTree import Element as element -from astropy.utils.data import get_pkg_data_filename -from pyvo.mivot.version_checker import check_astropy_version -from pyvo.mivot.viewer import MivotViewer -from pyvo.mivot.utils.exceptions import MivotError - - -@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") -def test_xml_viewer(m_viewer): - - m_viewer.next_row_view() - xml_viewer = m_viewer.xml_viewer - with pytest.raises(MivotError, - match="Cannot find dmrole wrong_role in any instances of the VOTable"): - xml_viewer.get_instance_by_role("wrong_role") - - with pytest.raises(MivotError, - match="Cannot find dmrole wrong_role in any instances of the VOTable"): - xml_viewer.get_instance_by_role("wrong_role", all_instances=True) - - with pytest.raises(MivotError, - match="Cannot find dmtype wrong_dmtype in any instances of the VOTable"): - xml_viewer.get_instance_by_type("wrong_dmtype") - - with pytest.raises(MivotError, - match="Cannot find dmtype wrong_dmtype in any instances of the VOTable"): - xml_viewer.get_instance_by_type("wrong_dmtype", all_instances=True) - - with pytest.raises(MivotError, - match="Cannot find dmrole wrong_role in any collections of the VOTable"): - xml_viewer.get_collection_by_role("wrong_role") - - with pytest.raises(MivotError, - match="Cannot find dmrole wrong_role in any collections of the VOTable"): - xml_viewer.get_collection_by_role("wrong_role", all_instances=True) - - instances_list_role = xml_viewer.get_instance_by_role("cube:MeasurementAxis.measure") - assert isinstance(instances_list_role, element) - - instances_list_role = xml_viewer.get_instance_by_role("cube:MeasurementAxis.measure", all_instances=True) - assert len(instances_list_role) == 3 - - instances_list_type = xml_viewer.get_instance_by_type("cube:Observable") - assert isinstance(instances_list_type, element) - - instances_list_type = xml_viewer.get_instance_by_type("cube:Observable", all_instances=True) - assert len(instances_list_type) == 3 - - collections_list_role = xml_viewer.get_collection_by_role("cube:NDPoint.observable") - assert isinstance(collections_list_role, element) - - collections_list_role = xml_viewer.get_collection_by_role("cube:NDPoint.observable", all_instances=True) - assert len(collections_list_role) == 1 - - -@pytest.fixture -def m_viewer(): - return MivotViewer(get_pkg_data_filename("data/test.mivot_viewer.xml"), - tableref="Results") diff --git a/pyvo/mivot/utils/vocabulary.py b/pyvo/mivot/utils/vocabulary.py index a7ec58b0..b03a7802 100644 --- a/pyvo/mivot/utils/vocabulary.py +++ b/pyvo/mivot/utils/vocabulary.py @@ -13,8 +13,6 @@ class Constant: FIRST_TABLE = "first_table" FIELD_UNIT = "field_unit" COL_INDEX = "col_index" - ROOT_COLLECTION = "root_collection" - ROOT_OBJECT = "root_object" NOT_SET = "NotSet" ANONYMOUS_TABLE = "AnonymousTable" diff --git a/pyvo/mivot/viewer/mivot_instance.py b/pyvo/mivot/viewer/mivot_instance.py index c642ea95..22bc9a71 100644 --- a/pyvo/mivot/viewer/mivot_instance.py +++ b/pyvo/mivot/viewer/mivot_instance.py @@ -10,7 +10,6 @@ Although attribute values can be changed by users, this class is first meant to provide a convenient access the mapped VOTable data """ -from pyvo.mivot.utils.vocabulary import Constant from pyvo.utils.prototype import prototype_feature from pyvo.mivot.utils.mivot_utils import MivotUtils from pyvo.mivot.utils.dict_utils import DictUtils @@ -75,9 +74,6 @@ def _create_class(self, **kwargs): """ for key, value in kwargs.items(): - # roles are used as key and the first element in a TEMPLATE has no role - if not key: - key = Constant.ROOT_OBJECT if isinstance(value, list): # COLLECTION setattr(self, self._remove_model_name(key), []) for item in value: @@ -186,6 +182,7 @@ def _get_class_dict(self, obj, classkey=None, slim=False, with_dmtypes=True): The serializable dictionary representation of the input. """ + # This case is likely not to occur because MIVOT does not support dictionaries if isinstance(obj, dict): data = {} for (k, v) in obj.items(): diff --git a/pyvo/mivot/viewer/mivot_viewer.py b/pyvo/mivot/viewer/mivot_viewer.py index 1a87f328..46485512 100644 --- a/pyvo/mivot/viewer/mivot_viewer.py +++ b/pyvo/mivot/viewer/mivot_viewer.py @@ -42,7 +42,6 @@ from pyvo.mivot.viewer.mivot_instance import MivotInstance from pyvo.utils.prototype import prototype_feature from pyvo.mivot.utils.mivot_utils import MivotUtils -from pyvo.mivot.viewer.xml_viewer import XMLViewer # Use defusedxml only if already present in order to avoid a new depency. try: from defusedxml import ElementTree as etree @@ -62,12 +61,15 @@ def __init__(self, votable_path, tableref=None, resolve_ref=False): Parameters ---------- - votable_path : str, parsed VOTable or DALResults instance - VOTable that will be parsed with the parser of Astropy, - which extracts the annotation block. + DALResults, VOTableFile or votable_path : Reference of the VOTable from which Astropy, + will extracts the annotation block. tableref : str, optional Used to identify the table to process. If not specified, the first table is taken by default. + resolve_ref : boolean + Ask for references between MIVOT instances to be resolved (referenced instances + are copied into the host object). This is usually used to copy the coordinates + systems into the object that uses them. Parameters ---------- resolve_ref : bool, optional @@ -99,7 +101,8 @@ def __init__(self, votable_path, tableref=None, resolve_ref=False): self._mapping_block = None self._mapped_tables = [] self._resource_seeker = None - self._dm_instance = None + self._dm_instances = [] + self._dm_globals_instances = [] self._resolve_ref = resolve_ref try: self._set_resource() @@ -107,7 +110,8 @@ def __init__(self, votable_path, tableref=None, resolve_ref=False): self._resource_seeker = ResourceSeeker(self._resource) self._set_mapped_tables() self._connect_table(tableref) - self._init_instance() + self._init_instances() + self._init_globals_instances() except MappingError as mnf: logging.error(str(mnf)) @@ -162,34 +166,38 @@ def dm_instance(self): """ returns ------- - A Python object (MivotInstance) built from the XML view of - the mapped model with attribute values set from the last values - of the last read data rows + MivotInstance: The Python object (MivotInstance) built from the XML view of the + first 'TEMPLATES' child, with the attribute values set according + to the values of the current read data row. """ - return self._dm_instance + dm_instances = self._dm_instances + return self.dm_instances[0] if dm_instances else None @property - def xml_view(self): + def dm_instances(self): """ - returns + Returns ------- - The XML view on the current data row + [MivotInstance]: The list of Python objects (MivotInstance) built from the XML views of + the TEMPLATES children, whose attribute values are set from the values + of the current read data row. """ - return self.xml_viewer.view + return self._dm_instances @property - def xml_viewer(self): + def dm_globals_instances(self): """ - returns - XMLViewer tuned to browse the TEMPLATES content + Returns + ------- + [MivotInstance]: The list of Python objects (MivotInstance) built from the XML views of + the GLOBALS children, whose attribute values are set from the values + of the current read data row. + This method allows to retrieve the GLOBALS (coordinates systems usually) + even when the viewer is in ``resolve_ref=False`` mode or if the reference + to the coordinates systems have not been setup in the objects representing + the mapped data. """ - # build a first XMLViewer for extract the content of the TEMPLATES element - model_view = XMLViewer(self._get_model_view()) - first_instance_dmype = self.get_first_instance_dmtype(tableref=self.connected_table_ref) - model_view.get_instance_by_type(first_instance_dmype) - - # return an XMLViewer tuned to process the TEMPLATES content - return XMLViewer(model_view._xml_view) + return self._dm_globals_instances @property def table_row(self): @@ -198,23 +206,23 @@ def table_row(self): def next_row_view(self): """ - jump to the next table row and update the MivotInstance instance + jump to the next table row and update the MivotInstance instance with the row values - returns + Returns ------- - MivotInstance: the updated instance or None - it he able end has been reached + [MivotInstance] + List of updated instances or None + it he able end has been reached """ self.next_table_row() if self._current_data_row is None: return None + self._init_instances() - if self._dm_instance is None: - xml_instance = self.xml_viewer.view - self._dm_instance = MivotInstance(**MivotUtils.xml_to_dict(xml_instance)) - self._dm_instance.update(self._current_data_row) - return self._dm_instance + for dm_instance in self._dm_instances: + dm_instance.update(self._current_data_row) + return self._dm_instances def get_table_ids(self): """ @@ -224,25 +232,6 @@ def get_table_ids(self): return None return self.resource_seeker.get_table_ids() - def get_globals_models(self): - """ - Get collection types in GLOBALS. - Collection types are GLOBALS/COLLECTION/INSTANCE@dmtype: - used for collections of static objects. - - Returns - ------- - dict - A dictionary containing the dmtypes of all the top-level INSTANCE/COLLECTION of GLOBALS. - The structure of the dictionary is {'COLLECTION': [dmtypes], 'INSTANCE': [dmtypes]}. - """ - if self._annotation_seeker is None: - return None - globals_models = {} - globals_models[Ele.COLLECTION] = self._annotation_seeker.get_globals_collection_dmtypes() - globals_models[Ele.INSTANCE] = self._annotation_seeker.get_globals_instance_dmtypes() - return globals_models - def get_models(self): """ Get a dictionary of models and their URLs. @@ -256,24 +245,6 @@ def get_models(self): return None return self._annotation_seeker.get_models() - def get_templates_models(self): - """ - Get dmtypes (except ivoa:..) of all INSTANCE/COLLECTION of all TEMPLATES. - Note: COLLECTION not implemented yet. - - Returns - ------- - dict: A dictionary containing dmtypes of all INSTANCE/COLLECTION of all TEMPLATES. - The format is {'tableref': {'COLLECTIONS': [dmtypes], 'INSTANCE': [dmtypes]}, ...}. - """ - if self._annotation_seeker is None: - return None - templates_models = {} - gni = self._annotation_seeker.get_instance_dmtypes()[Ele.TEMPLATES] - for tid, tmplids in gni.items(): - templates_models[tid] = {Ele.COLLECTION: [], Ele.INSTANCE: tmplids} - return templates_models - def next_table_row(self): """ Iterate once on the table row @@ -293,48 +264,47 @@ def rewind(self): if self._table_iterator: self._table_iterator.rewind() - def get_first_instance_dmtype(self, tableref=None): + def _get_templates_child_instances(self, tableref=None): """ - Return the dmtype of the head INSTANCE (first TEMPLATES child). - If no INSTANCE is found, take the first COLLECTION. + Returns + ------- + [`xml.etree.ElementTree.Element`] + List of all INSTANCES elements children of the current TEMPLATES block + """ + if self._annotation_seeker is None: + return None + templates_block = self._annotation_seeker.get_templates_block(tableref) + return XPath.x_path(templates_block, ".//" + Ele.INSTANCE) + + def get_dm_instance_dmtypes(self, tableref): + """ + Return the dmtypes of the INSTANCEs children of the + TEMPLATES block mapping the data table identified by tableref. Parameters ---------- - tableref : str or None, optional - Identifier of the table. + tableref : str or None + Identifier of the data table. + Returns ------- - ~`xml.etree.ElementTree.Element` - The first child of TEMPLATES. + [string] + list of dmtypes + + Raises + ------ + MivotError + if no INSTANCE can be found """ - if self._annotation_seeker is None: - return None - child_template = self._annotation_seeker.get_templates_block(tableref) - child = child_template.findall("*") - collection = XPath.x_path(self._annotation_seeker.get_templates_block(tableref), - ".//" + Ele.COLLECTION) - instance = XPath.x_path(self._annotation_seeker.get_templates_block(tableref), ".//" + Ele.INSTANCE) - if len(collection) >= 1: - collection[0].set(Att.dmtype, Constant.ROOT_COLLECTION) - (self._annotation_seeker.get_templates_block(tableref).find(".//" + Ele.COLLECTION) - .set(Att.dmtype, Constant.ROOT_COLLECTION)) - if len(child) > 1: - if len(instance) >= 1: - for inst in instance: - if inst in child: - return inst.get(Att.dmtype) - elif len(collection) >= 1: - for coll in collection: - if coll in child: - return coll.get(Att.dmtype) - elif len(child) == 1: - if child[0] in instance: - return child[0].get(Att.dmtype) - elif child[0] in collection: - return collection[0].get(Att.dmtype) - else: - raise MivotError("Can't find the first " + Ele.INSTANCE - + "/" + Ele.COLLECTION + " in " + Ele.TEMPLATES) + dmtypes = [] + templates_block = self._annotation_seeker.get_templates_block(tableref) + instances = XPath.x_path(templates_block, ".//" + Ele.INSTANCE) + for instance in instances: + dmtypes.append(instance.get(Att.dmtype)) + + if not dmtypes: + raise MivotError("Can't find " + Ele.INSTANCE + " in " + Ele.TEMPLATES) + return dmtypes def _connect_table(self, tableref=None): """ @@ -379,12 +349,20 @@ def _connect_table(self, tableref=None): self._set_column_indices() self._set_column_units() - def _get_model_view(self): + def _get_model_view(self, xml_instance): """ Return an XML model view of the last read row. - This function resolves references by default. + + - References are possibly resolved here. + - ``ATTRIBUTE@value`` are set with actual data row values + + Returns + ------- + `xml.etree.ElementTree.Element` + XML model view of the last read row. + """ - templates_copy = deepcopy(self._templates) + templates_copy = deepcopy(xml_instance) if self._resolve_ref is True: while StaticReferenceResolver.resolve(self._annotation_seeker, self._connected_tableref, templates_copy) > 0: @@ -397,7 +375,6 @@ def _get_model_view(self): XmlUtils.add_column_units(templates_copy, self._resource_seeker .get_id_unit_mapping(self._connected_tableref)) - # for ele in templates_copy.xpath("//ATTRIBUTE"): for ele in XPath.x_path(templates_copy, ".//ATTRIBUTE"): ref = ele.get(Att.ref) if ref is not None and ref != Constant.NOT_SET and Constant.COL_INDEX in ele.attrib: @@ -405,25 +382,45 @@ def _get_model_view(self): ele.attrib[Att.value] = str(self._current_data_row[int(index)]) return templates_copy - def _init_instance(self): + def _init_instances(self): """ - Read the first table row and build the MivotInstance (_instance attribute) from it. - The table row iterator in rewind at he end to make sure we won't lost the first data row. + Read the first table row and build all MivotInstances (_dm_instances attribute) from it. + The table row iterator in rewind at the end to make sure we won't lost the first data row. """ - if self._dm_instance is None: + if not self._dm_instances: self.next_table_row() - first_instance = self.get_first_instance_dmtype(tableref=self.connected_table_ref) - xml_instance = self.xml_viewer.get_instance_by_type(first_instance) - self._dm_instance = MivotInstance(**MivotUtils.xml_to_dict(xml_instance)) + xml_instances = self._get_templates_child_instances(self.connected_table_ref) + self._dm_instances = [] + for xml_instance in xml_instances: + self._dm_instances.append( + MivotInstance( + **MivotUtils.xml_to_dict(self._get_model_view(xml_instance)) + )) self.rewind() - return self._dm_instance + + def _init_globals_instances(self): + """ + Build one MivotInstance for each GLOBALS/INSTANCE. Internal references are always resolved + Globals MivotInstance are stored in the _dm_globals_instances list + """ + if not self._dm_globals_instances: + globals_copy = deepcopy(self._annotation_seeker.globals_block) + + while StaticReferenceResolver.resolve(self._annotation_seeker, None, + globals_copy) > 0: + pass + for ele in XPath.x_path(globals_copy, "./" + Ele.INSTANCE): + self._dm_globals_instances.append( + MivotInstance( + **MivotUtils.xml_to_dict(ele) + )) def _set_mapped_tables(self): """ - Set the mapped tables with a list of the TEMPLATES tablerefs. + Set the _mapped_tables list with the TEMPLATES tablerefs. """ if not self.resource_seeker: - self._mapped_table = [] + self._mapped_tables = [] else: self._mapped_tables = self._annotation_seeker.get_templates() @@ -478,7 +475,7 @@ def _squash_join_and_references(self): def _set_column_indices(self): """ Add column ranks to attribute having a ref. - Using ranks allow identifying columns even numpy raw have been serialised as [] + Using ranks allow identifying columns even when numpy raw have been serialised as [] """ index_map = self._resource_seeker.get_id_index_mapping(self._connected_tableref) XmlUtils.add_column_indices(self._templates, index_map) diff --git a/pyvo/mivot/viewer/xml_viewer.py b/pyvo/mivot/viewer/xml_viewer.py deleted file mode 100644 index fd69e79c..00000000 --- a/pyvo/mivot/viewer/xml_viewer.py +++ /dev/null @@ -1,145 +0,0 @@ -# Licensed under a 3-clause BSD style license - see LICENSE.rst -""" -XMLViewer provides several getters on XML instances built by - `pyvo.mivot.viewer.mivot_viewer`. -""" -from pyvo.mivot.utils.exceptions import MivotError -from pyvo.mivot.utils.xpath_utils import XPath -from pyvo.utils.prototype import prototype_feature - - -@prototype_feature('MIVOT') -class XMLViewer: - """ - The XMLViewer is used by `~pyvo.mivot.viewer.mivot_viewer` - to extract from the XML serialization of the model, - elements that will be used to build the dictionary from which - the Python class holding the mapped model will be generated. - """ - def __init__(self, xml_view): - self._xml_view = xml_view - - @property - def view(self): - """ - getter returning the XML model view - - returns - ------- - XML model view to be parsed - by different methods - """ - return self._xml_view - - def get_instance_by_role(self, dmrole, all_instances=False): - """ - If all_instances is False, return the first INSTANCE matching with @dmrole. - If all_instances is True, return a list of all instances matching with @dmrole. - Parameters - ---------- - dmrole : str - The @dmrole to look for. - all_instances : bool, optional - If True, returns a list of all instances, otherwise returns the first instance. - Default is False. - Returns - ------- - Union[`xml.etree.ElementTree.Element`, List[`xml.etree.ElementTree.Element`], None] - If all_instances is False, returns the instance matching with @dmrole. - If all_instances is True, returns a list of all instances matching with @dmrole. - If no matching instance is found, returns None. - Raises - ------ - MivotElementNotFound - If dmrole is not found. - """ - instances = XPath.select_elements_by_atttribute( - self._xml_view, - "INSTANCE", - "dmrole", - dmrole) - - if len(instances) == 0: - raise MivotError( - f"Cannot find dmrole {dmrole} in any instances of the VOTable") - - if all_instances is False: - return instances[0] - else: - return instances - - def get_instance_by_type(self, dmtype, all_instances=False): - """ - Return the instance matching with @dmtype. - If all_instances is False, returns the first INSTANCE matching with @dmtype. - If all_instances is True, returns a list of all instances matching with @dmtype. - Parameters - ---------- - dmtype : str - The @dmtype to look for. - all : bool, optional - If True, returns a list of all instances, otherwise returns the first instance. - Default is False. - Returns - ------- - Union[~`xml.etree.ElementTree.Element`, List[~`xml.etree.ElementTree.Element`], None] - If all_instances is False, returns the instance matching with @dmtype. - If all_instances is True, returns a list of all instances matching with @dmtype. - If no matching instance is found, returns None. - Raises - ------ - MivotElementNotFound - If dmtype is not found. - """ - instances = XPath.select_elements_by_atttribute( - self._xml_view, - "INSTANCE", - "dmtype", - dmtype) - - if len(instances) == 0: - raise MivotError( - f"Cannot find dmtype {dmtype} in any instances of the VOTable") - - if all_instances is False: - return instances[0] - else: - return instances - - def get_collection_by_role(self, dmrole, all_instances=False): - """ - Return the collection matching with @dmrole. - If all_instances is False, returns the first COLLECTION matching with @dmrole. - If all_instances is True, returns a list of all COLLECTION matching with @dmrole. - Parameters - ---------- - dmrole : str - The @dmrole to look for. - all_instances : bool, optional - If True, returns a list of all COLLECTION, otherwise returns the first COLLECTION. - Default is False. - Returns - ------- - Union[~`xml.etree.ElementTree.Element`, List[~`xml.etree.ElementTree.Element`], None] - If all_instances is False, returns the collection matching with @dmrole. - If all_instances is True, returns a list of all collections matching with @dmrole. - If no matching collection is found, returns None. - Raises - ------ - MivotElementNotFound - If dmrole is not found. - """ - collections = XPath.select_elements_by_atttribute( - self._xml_view, - "COLLECTION", - "dmrole", - dmrole) - - if len(collections) == 0: - raise MivotError( - f"Cannot find dmrole {dmrole} in any collections of the VOTable") - - if all_instances is False: - return collections[0] - else: - return collections diff --git a/pyvo/mivot/writer/annotations.py b/pyvo/mivot/writer/annotations.py index d1a2e57b..a85279ef 100644 --- a/pyvo/mivot/writer/annotations.py +++ b/pyvo/mivot/writer/annotations.py @@ -170,7 +170,7 @@ def build_mivot_block(self, *, templates_id=None, schema_check=True): ---------- templates_id : str, optional The ID to associate with the block. Defaults to None. - schema_check : boolean, optional + schema_check : boolean, optional (default True) Skip the XSD validation if False (use to make test working in local mode). Raises diff --git a/pyvo/mivot/writer/instances_from_models.py b/pyvo/mivot/writer/instances_from_models.py index f69a5105..08c0269c 100644 --- a/pyvo/mivot/writer/instances_from_models.py +++ b/pyvo/mivot/writer/instances_from_models.py @@ -696,17 +696,20 @@ def add_query_origin(self, mapping={}): self._annotation._dmids.append("_origin") return query_origin_instance - def pack_into_votable(self, *, report_msg="", sparse=False): + def pack_into_votable(self, *, report_msg="", sparse=False, schema_check=True): """ Pack all mapped objects in the annotation block and put it in the VOTable. Parameters ---------- - report_msg: string, optional (default to an empty string) + report_msg : string, optional (default to an empty string) Content of the REPORT Mivot tag - sparse: boolean, optional (default to False) + sparse : boolean, optional (default to False) If True, all properties are added in a independent way to the the TEMPLATES. They are packed in a MangoObject otherwise. + schema_check : boolean, optional (default to True) + If True the MIVOT block is validated against its schema. + This may test failing due to remote accesses. """ self._annotation.set_report(True, report_msg) if sparse is True: @@ -718,5 +721,5 @@ def pack_into_votable(self, *, report_msg="", sparse=False): self._annotation.add_templates(self._mango_instance.get_mango_object( with_origin=("_origin" in self._annotation._dmids))) - self._annotation.build_mivot_block() + self._annotation.build_mivot_block(schema_check=schema_check) self._annotation.insert_into_votable(self._votable, override=True) diff --git a/pyvo/mivot/writer/mivot-v1.xsd b/pyvo/mivot/writer/mivot-v1.xsd index ae80f5ba..28d19e09 100644 --- a/pyvo/mivot/writer/mivot-v1.xsd +++ b/pyvo/mivot/writer/mivot-v1.xsd @@ -9,9 +9,9 @@ - - - + + + diff --git a/pyvo/mivot/writer/v1.1.xsd b/pyvo/mivot/writer/v1.1.xsd new file mode 100644 index 00000000..7755bacd --- /dev/null +++ b/pyvo/mivot/writer/v1.1.xsd @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Deprecated in Version 1.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyvo/mivot/writer/v1.2.xsd b/pyvo/mivot/writer/v1.2.xsd new file mode 100644 index 00000000..810c4491 --- /dev/null +++ b/pyvo/mivot/writer/v1.2.xsd @@ -0,0 +1,562 @@ + + + + + VOTable1.2 is meant to serialize tabular documents in the + context of Virtual Observatory applications. This schema + corresponds to the VOTable document available from + http://www.ivoa.net/Documents/latest/VOT.html + + + + + + + + + + + + + + + + + + + + Accept UCD1+ + Accept also old UCD1 (but not / + %) including SIAP convention (with :) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + content-role was previsouly restricted as: + + + + + + + + + ]]>; is now a name token. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Deprecated in Version 1.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Deprecated in Version 1.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Added in Version 1.2: INFO for diagnostics + + + + + + + + + + + + + + + + + + + + + + + + The 'encoding' attribute is added here to avoid + problems of code generators which do not properly + interpret the TR/TD structures. + 'encoding' was chosen because it appears in + appendix A.5 + + + + + + + + + The ID attribute is added here to the TR tag to avoid + problems of code generators which do not properly + interpret the TR/TD structures + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Added in Version 1.2: INFO for diagnostics + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Added in Version 1.2: INFO for diagnostics in several places + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyvo/mivot/writer/v1.3.xsd b/pyvo/mivot/writer/v1.3.xsd new file mode 100644 index 00000000..0979a2a3 --- /dev/null +++ b/pyvo/mivot/writer/v1.3.xsd @@ -0,0 +1,635 @@ + + + + + VOTable is meant to serialize tabular documents in the + context of Virtual Observatory applications. This schema + corresponds to the VOTable document available from + http://www.ivoa.net/Documents/latest/VOT.html + + + + + + + + + + + + + + + + + + + + Accept UCD1+ + Accept also old UCD1 (but not / + %) including SIAP convention (with :) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + content-role was previsouly restricted as: + + + + + + + + + ]]>; is now a token. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Values for this attribute must be taken from the IVOA + refframe vocabulary, http://www.ivoa.net/rdf/refframe + + + + + + + The reference position SHOULD be taken from the IVOA + refposition vocabulary (http://www.ivoa.net/rdf/refposition). + + + + + + + + + + + This is a time origin of a time coordinate, given as a + Julian Date for the the time scale and reference point + defined. It is usually given as a floating point + literal; for convenience, the magic strings “MJD-origin” + (standing for 2400000.5) and “JD-origin” (standing for 0) + are also allowed. + + + + + + + + + + + + + + + + The time origin is the offset or the time coordinate to Julian + Date. The timeorigin attribute MUST be given unless the time's + representation contains a year of a calendar era, in which case it + MUST NOT be present. + + + + + + + This is the time scale used. Values SHOULD be + taken from the IVOA timescale vocabulary (http://www.ivoa.net/rdf/timescale). + + + + + + + The reference position SHOULD be taken from the IVOA + refposition vocabulary (http://www.ivoa.net/rdf/refposition). + + + + + + + + + + Deprecated in Version 1.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Added in Version 1.2: INFO for diagnostics + + + + + + + + + + + + + + + + + + + + + + + + + The 'encoding' attribute is added here to avoid + problems of code generators which do not properly + interpret the TR/TD structures. + 'encoding' was chosen because it appears in + appendix A.5 + + + + + + + + + The ID attribute is added here to the TR tag to avoid + problems of code generators which do not properly + interpret the TR/TD structures + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Added in Version 1.2: INFO for diagnostics + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Added in Version 1.2: INFO for diagnostics in several places + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +