Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature MIVOT (Model Instance in VOTable) #497

Merged
merged 56 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
c10d4d2
add feature MIVOT
somilia Oct 31, 2023
76d630a
remove commented function
somilia Oct 31, 2023
bc6d45e
removed useless function
somilia Oct 31, 2023
f3b9cb0
fix issues and add docstrings
somilia Nov 3, 2023
28ee09a
update votable file
somilia Nov 7, 2023
a9fb04f
fix Sphinx complains
lmichel Nov 11, 2023
3de7b1e
changed sphinx documentation
somilia Nov 13, 2023
e558251
changed docstring in numpydoc style
somilia Nov 14, 2023
12273f1
remove tests including dynamic reference
somilia Nov 15, 2023
fb68cd2
fix code style
somilia Nov 15, 2023
c805156
add astropy version required
somilia Nov 16, 2023
6bf3eda
replaced lxml with defusedxml/xml
somilia Nov 22, 2023
7a87fca
add epoch propagation
somilia Nov 23, 2023
25a06ce
improve MivotClass and EpochProp
somilia Nov 29, 2023
a39db4f
Various edits on github web interface following first round of review
lmichel Nov 30, 2023
23f5e46
improve epoch_prop
somilia Nov 30, 2023
a7fe9c7
add convertion to astropy unit
somilia Dec 1, 2023
7d2c489
add tests epochPropagation
somilia Dec 4, 2023
8ce9049
add reference system
somilia Dec 5, 2023
0df4a4e
add cast type in mivotclass
somilia Dec 7, 2023
d221081
multiple commits adding examples to index.rst
lmichel Dec 7, 2023
4c70e53
rename ModelViewer
somilia Dec 7, 2023
a701f69
cast numpy.float32 into float
somilia Dec 8, 2023
9fc1578
replace year with yr in ATTRIBUTE@unit
lmichel Jan 30, 2024
c881ef4
Change of the column index map: {name{indx, ID, ref}...}. This was
lmichel Jan 30, 2024
b90d409
Set ATTRIBUTE@ref with column IDs taken into account that columns
lmichel Jan 30, 2024
477f0ae
little changes for testing
lmichel Jan 30, 2024
3950b1f
remove debug printout
lmichel Jan 30, 2024
83ce2ca
change exception message in assert
lmichel Jan 30, 2024
c2aa8dd
take into account the new format of the column index
lmichel Jan 30, 2024
8ba6499
unit test based on the annotated VOTables served by Vizier
lmichel Jan 30, 2024
3571933
Handle the ATTRIBUTE without @ref
lmichel Jan 30, 2024
12cb170
skip test for old Astropy versions. Import ErfaWarning from erfa instead
lmichel Jan 31, 2024
4617071
give a name ("mivot") the logger in order not to override the root
lmichel Jan 31, 2024
8f2edfb
constructor either accept the VOTable path (string) or a DALResults
lmichel Jan 31, 2024
9aaf90e
update doc string
lmichel Jan 31, 2024
59d5c52
Add accessors to build-in ModelViewerLevel2/3 instances
lmichel Jan 31, 2024
50c142b
do the proper Erfa import according the the Astropy version
lmichel Feb 2, 2024
9816dd4
some rephrasing + TOC containig the example page
lmichel Feb 2, 2024
bbb193f
More details about how the mivot package does work
lmichel Feb 2, 2024
b825a34
VOTable sample for tests moved to dm-usecases repo. Class
lmichel Feb 19, 2024
42c6570
api suggested by Gilles
lmichel Feb 22, 2024
912a260
little pylint pass
lmichel Feb 22, 2024
2c54c3f
Reorganisation of the API. All logic is controlled from the MivotViewer
lmichel Mar 2, 2024
a8288ef
bring test data sample back from the external repo in oder to restore
lmichel Mar 7, 2024
7fb4437
Reviewer request: Add a watchout stating that the current implementation
lmichel Apr 17, 2024
e5d267a
TD Review: 1) package specific loader removed 2) make VOTable sample …
lmichel Apr 28, 2024
95f481a
Pre-review: 1) fix singleton-comparison 2) commented code deleted 3) …
lmichel Apr 28, 2024
5f9c371
AD Review: 1) instance renamed as mivot_block 2) ignore coverage.xml…
lmichel May 2, 2024
3634d05
AD Review: false issue : 1) check astropy only when Mivot code run 2)…
lmichel May 3, 2024
5fd899a
make test-user-api more useful for general users
lmichel May 6, 2024
4bd4eaf
AD Review: add a test showing up a full EpochPosition instance (position
lmichel May 6, 2024
ddc7543
Fix an inconsistency in the MIVOT instance key generation ("." in keys).
lmichel May 6, 2024
8579e17
Minor fixes for after-rebase review
bsipocz May 7, 2024
713f452
Adding changelog
bsipocz May 7, 2024
57a3885
Update CHANGES.rst
lmichel May 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ pip-wheel-metadata
.project
.pydevproject
.settings
coverage.xml

# Mac OSX
.DS_Store

# ipython
.ipynb_checkpoints
/.pytest_cache/
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Enhancements and Fixes
- registry.Ivoid now accepts multiple ivoids and will then match any of
them. [#517]

- Introducing the new MIVOT module, enabling processed VOTable data mapped to
any model serialized in VO-DML. This package dynamically generates python objects
whose structure corresponds to the classes of the mapped models. [#497]

Deprecations and Removals
-------------------------

Expand Down
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def pytest_configure(config):
# packages for which version numbers are displayed when running the tests.
PYTEST_HEADER_MODULES['Astropy'] = 'astropy' # noqa
PYTEST_HEADER_MODULES['requests'] = 'requests' # noqa
PYTEST_HEADER_MODULES['defusedxml'] = 'defusedxml'

PYTEST_HEADER_MODULES.pop('Pandas', None)
PYTEST_HEADER_MODULES.pop('h5py', None)
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,6 @@ Using ``pyvo``
registry/index
io/index
auth/index
mivot/index
utils/index
utils/prototypes
Binary file added docs/mivot/_images/mangoEpochPosition.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
235 changes: 235 additions & 0 deletions docs/mivot/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
********************
MIVOT (`pyvo.mivot`)
********************

This module contains the new feature of annotations in VOTable.
Astropy version >= 6.0 is required.

Introduction
============
.. pull-quote::

Model Instances in VOTables (MIVOT) defines a syntax to map VOTable
data to any model serialized in VO-DML. The annotation operates as a
bridge between the data and the model. It associates the column/param
metadata from the VOTable to the data model elements (class, attributes,
types, etc.) [...].
The data model elements are grouped in an independent annotation block
complying with the MIVOT XML syntax. This annotation block is added
as an extra resource element at the top of the VOTable result resource. The
MIVOT syntax allows to describe a data structure as a hierarchy of classes.
It is also able to represent relations and composition between them. It can
also build up data model objects by aggregating instances from different
tables of the VOTable.

- Model Instances in VOTables is a VO `standard <https://ivoa.net/documents/MIVOT/20230620/REC-mivot-1.0.pdf>`_
- Requires Astropy>=6.0
- ``pyvo.mivot`` is a prototype feature which must be activated with ``activate_features("MIVOT")``


Implementation Scope
--------------------
This implementation is totally model-agnostic.

- It does not operate any validation against specific data models.
- It just requires the annotation syntax being compliant with the standards.

However, many data samples used for the test suite and provided as examples
are based on the ``EpochPropagation`` class of the ``Mango`` data model
that is still a draft.
This class collects all the parameters we need to compute the epoch propagation of moving sky objects.
Some of the examples have been provided by a special end-point of the Vizier cone-search service
(https://cdsarc.cds.unistra.fr/beta/viz-bin/mivotconesearch) that maps query results to this model.

.. image:: _images/mangoEpochPosition.png
:width: 500
:alt: EpochPropagation class used to validate this api.

It is to be noted that the Vizier service does not annotate errors at the time of writing (Q1 2024)

The implementation uses the Astropy read/write annotation module (6.0+),
which allows to get (and set) Mivot blocks from/into VOTables as an XML element serialized as a string.

.. pull-quote::

Not all MIVOT features are supported by this implementation, which mainly focuses on the
epoch propagation use case:

- ``JOIN`` features are not supported.
- ``TEMPLATES`` with more than one ``INSANCE`` not supported.

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, resulting from a cone-search query which data are mapped
to the ``EpochPosition`` class, can be consumed.

.. 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://cdsarc.cds.unistra.fr/beta/viz-bin/mivotconesearch/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
... )
... )
>>> mivot_instance = m_viewer.dm_instance
>>> print(mivot_instance.dmtype)
EpochPosition
>>> print(mivot_instance.Coordinate_coordSys.spaceRefFrame.value)
ICRS
>>> while m_viewer.next():
... 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:

- ``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``.

.. 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

mivot_instance = m_viewer.dm_instance
mivot_object_dict = mivot_object.dict

DictUtils.print_pretty_json(mivot_object_dict)
{
"dmtype": "EpochPosition",
"longitude": {"value": 359.94372764, "unit": "deg"},
"latitude": {"value": -0.28005255, "unit": "deg"},
"pmLongitude": {"value": -5.14, "unit": "mas/yr"},
"pmLatitude": {"value": -25.43, "unit": "mas/yr"},
"epoch": {"value": 1991.25, "unit": "year"},
"Coordinate_coordSys": {
"dmtype": "SpaceSys",
"dmid": "SpaceFrame_ICRS",
"dmrole": "coordSys",
"spaceRefFrame": {"value": "ICRS"},
},
}

- 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
string serialization of this dictionary.

Per-Row Readout
---------------

The annotation schema can also be applied to table rows read outside of the ``MivotViewer``
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 = []
for rec in table.array:
mivot_object.update(rec)
read.append(mivot_object.longitude.value)
# show that the model retrieve the correct data values
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.

For XML Hackers
---------------

The model instances can also be serialized as XML elements that can be parsed with XPath queries.

.. 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():
xml_view = mivot_viewer.xml_view
# do whatever you want with this XML element

It 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.

Class Generation in a Nutshell
------------------------------

MIVOT reconstructs model structures with 3 elements:

- ``INSTANCE`` for the objects
- ``ATTRIBUTE`` for the attributes
- ``COLLECTION`` for the elements with a cardinality greater than 1

The role played by each of these elements in the model hierarchy is defined
by its ``@dmrole`` XML attribute. Types of both ``INSTANCE`` and ``ATTRIBUTE`` are defined by
their ``@dmtype`` XML attributes.

``MivotInstance`` classes are built by following MIVOT annotation structure:

- ``INSTANCE`` are represented by Python classes
- ``ATTRIBUTE`` are represented by Python class fields
- ``COLLECTION`` are represented by Python lists ([])

``@dmrole`` and ``@dmtype`` cannot be used as Python keywords as such, because they are built from VO-DML
identifiers, which have the following structure: ``model:a.b``.

- Only the last part of the path is kept for attribute names.
- For class names, forbidden characters (``:`` or ``.``) are replaced with ``_``.
- 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.
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 <https://docs.python.org/3/reference/datamodel.html>`_).

.. code-block:: python
:caption: Exploring the MivotInstance structure with the internal dictionaries

mivot_instance = mivot_viewer.dm_instance

print(mivot_instance.__dict__.keys())
dict_keys(['dmtype', 'longitude', 'latitude', 'pmLongitude', 'pmLatitude', 'epoch', 'Coordinate_coordSys'])

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'])

Reference/API
=============

.. automodapi:: pyvo.mivot
.. automodapi:: pyvo.mivot.viewer
.. automodapi:: pyvo.mivot.seekers
.. automodapi:: pyvo.mivot.features
.. automodapi:: pyvo.mivot.utils
1 change: 1 addition & 0 deletions pyvo/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def pytest_configure(config):
PYTEST_HEADER_MODULES.pop('h5py', None)
PYTEST_HEADER_MODULES.pop('Scipy', None)
PYTEST_HEADER_MODULES.pop('Matplotlib', None)
PYTEST_HEADER_MODULES['defusedxml'] = 'defusedxml'

from . import __version__
TESTED_VERSIONS['pyvo'] = __version__
2 changes: 2 additions & 0 deletions pyvo/mivot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# package entry point
from .viewer.mivot_viewer import MivotViewer
Empty file added pyvo/mivot/features/__init__.py
Empty file.
71 changes: 71 additions & 0 deletions pyvo/mivot/features/static_reference_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Class used to resolve each static REFERENCE found in mivot_block.
"""
from copy import deepcopy
from pyvo.mivot.utils.exceptions import MivotException
from pyvo.mivot.utils.xpath_utils import XPath
from pyvo.utils.prototype import prototype_feature


@prototype_feature('MIVOT')
class StaticReferenceResolver:
"""
Namespace for the function processing the static REFERENCEs
"""
@staticmethod
def resolve(annotation_seeker, templates_ref, mivot_block):
"""
Resolve all static REFERENCEs found in the mivot_block.
The referenced objects are first searched in GLOBALS and then in the templates_ref table.
REFERENCE elements are replaced with the referenced objects set with the roles of the REFERENCEs.
Works even if REFERENCE tags are numbered by the former processing.
Parameters
----------
annotation_seeker : AnnotationSeeker
Utility to extract desired elements from the mapping block.
templates_ref : str
Identifier of the table where the mivot_block comes from.
mivot_block : xml.etree.ElementTree
The XML element object.
Returns
-------
int
The number of references resolved.
Raises
------
MappingException
If the reference cannot be resolved.
NotImplementedError
If the reference is dynamic.
"""
resolved_refs = 0
for ele in XPath.x_path_startwith(mivot_block, './/REFERENCE_'):
dmref = ele.get("dmref")
# If we have no @dmref in REFERENCE, we consider this is a ref based on a keys
if dmref is None:
raise NotImplementedError("Dynamic reference not implemented")
target = annotation_seeker.get_globals_instance_by_dmid(dmref)
found_in_global = True
if target is None and templates_ref is not None:
target = annotation_seeker.get_templates_instance_by_dmid(templates_ref, dmref)
found_in_global = False
if target is None:
raise MivotException(f"Cannot resolve reference={dmref}")
# Resolve static references recursively
if not found_in_global:
StaticReferenceResolver.resolve(annotation_seeker, templates_ref, ele)
else:
StaticReferenceResolver.resolve(annotation_seeker, None, ele)
# Set the reference role to the copied instance
target_copy = deepcopy(target)
# If the reference is within a collection: no role
if ele.get('dmrole'):
target_copy.attrib["dmrole"] = ele.get('dmrole')
parent_map = {c: p for p in mivot_block.iter() for c in p}
parent = parent_map[ele]
# Insert the referenced object
parent.append(target_copy)
# Drop the reference
parent.remove(ele)
resolved_refs += 1
return resolved_refs
4 changes: 4 additions & 0 deletions pyvo/mivot/seekers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
``seekers`` package contains utilities for retrieving
components of VOTales or of Mivot blocks
"""
Loading
Loading