Source code for pyvo.mivot.writer.header_mapper
"""
``HeaderMapper`` class source
"""
import logging
from pyvo.mivot.glossary import Roles, EpochPositionAutoMapping
from pyvo.mivot.utils.dict_utils import DictUtils
[docs]
class HeaderMapper:
"""
This utility class generates dictionaries from header elements of a VOTable.
These dictionaries are used as input parameters by `pyvo.mivot.writer.InstancesFromModels`
to create MIVOT instances that are placed in the GLOBALS block or in the TEMPLATES.
In the current implementation, the following elements can be extracted:
- COOSYS -> coords:SpaceSys
- TIMESYS - coords:TimeSys
- INFO -> mango:QueryOrigin
- FIELD -> mango:EpochPosition
"""
def __init__(self, votable):
"""
Constructor parameters:
Parameters
----------
votable : astropy.io.votable.tree.VOTableFile
parsed votable from which INFO element are processed
"""
self._votable = votable
def _check_votable_head_element(self, parameter):
"""
Check that the parameter is a valid value.
.. note::
Vizier uses the ``UNKNOWN`` word to tag not set values
"""
return parameter is not None and parameter != "UNKNOWN"
def _extract_query_origin(self):
"""
Create a mapping dictionary from ``INFO`` elements found
in the VOTable header.
This dictionary is used to populate the ``mango:QueryOrigin`` attributes
Returns
-------
dict
Dictionary that is part of the input parameter for
:py:meth:`pyvo.mivot.writer.InstancesFromModels.add_query_origin`
"""
mapping = {}
for info in self._votable.infos:
if info.name in Roles.QueryOrigin:
mapping[info.name] = info.value
return mapping
def _extract_data_origin(self, resource):
"""
Create a mapping dictionary from ``INFO`` elements found
in the header of the first VOTable resource.
This dictionary is used to populate the ``mango:QueryOrigin`` part of
the ``mango:QueryOrigin`` instance.
Returns
-------
dict
Dictionary that is part of the input parameter for
:py:meth:`pyvo.mivot.writer.InstancesFromModels.add_query_origin`
"""
mapping = {}
article = None
for info in resource.infos:
if info.name in Roles.DataOrigin or info.name == "creator":
if DictUtils.add_array_element(mapping, "dataOrigin"):
article = {}
if info.name != "creator":
article[info.name] = info.value
else:
DictUtils.add_array_element(article, "creators")
article["creators"].append(info.value)
for info in resource.infos:
art_ref = {}
if info.name in Roles.Article:
DictUtils.add_array_element(article, "articles")
art_ref[info.name] = info.value
if art_ref:
article["articles"].append(art_ref)
mapping["dataOrigin"].append(article)
return mapping
[docs]
def extract_origin_mapping(self):
"""
Create a mapping dictionary from all VOTable ``INFO`` elements.
This dictionary is used to build a ``mango:QueryOrigin`` INSTANCE
- INFO elements located in the VOTable header are used to build the ``mango:QueryOrigin``
part which scope is the whole VOtable by construction (one query -> one VOTable)
- INFO elements located in the resource header are used to build the ``mango:DataOrigin``
part which scope is the data located in this resource.
Returns
-------
dict
Dictionary that can be used as input parameter for
:py:meth:`pyvo.mivot.writer.InstancesFromModels.add_query_origin`
"""
mapping = self._extract_query_origin()
for resource in self._votable.resources:
mapping = {**mapping, **self._extract_data_origin(resource)}
return mapping
[docs]
def extract_coosys_mapping(self):
"""
Create a mapping dictionary for each ``COOSYS`` element found
in the first VOTable resource.
Returns
-------
[dict]
Array of dictionaries which items can be used as input parameter for
:py:meth:`pyvo.mivot.writer.InstancesFromModels.add_simple_space_frame`
"""
mappings = []
for resource in self._votable.resources:
for coordinate_system in resource.coordinate_systems:
mapping = {}
if not self._check_votable_head_element(coordinate_system.system):
logging.warning(f"Not valid COOSYS found: ignored in MIVOT: {coordinate_system}")
continue
mapping["spaceRefFrame"] = coordinate_system.system
if self._check_votable_head_element(coordinate_system.equinox):
mapping["equinox"] = coordinate_system.equinox
if self._check_votable_head_element(coordinate_system.epoch):
mapping["epoch"] = coordinate_system.epoch
mappings.append(mapping)
return mappings
[docs]
def extract_timesys_mapping(self):
"""
Create a mapping dictionary for each ``TIMESYS`` element found
in the first VOTable resource.
.. note::
the ``origin`` attribute is not supported yet
Returns
-------
[dict]
Array of dictionaries which items can be used as input parameter for
:py:meth:`pyvo.mivot.writer.InstancesFromModels.add_simple_time_frame`
"""
mappings = []
for resource in self._votable.resources:
for time_system in resource.time_systems:
mapping = {}
if not self._check_votable_head_element(time_system.timescale):
logging.warning(f"Not valid TIMESYS found: ignored in MIVOT: {time_system}")
continue
mapping["timescale"] = time_system.timescale
if self._check_votable_head_element(time_system.refposition):
mapping["refPosition"] = time_system.refposition
mappings.append(mapping)
return mappings
[docs]
def extract_epochposition_mapping(self):
"""
Analyze the FIELD UCD-s to infer a data mapping to the EpochPosition class.
This mapping covers the 6 parameters with the Epoch and their errors.
The correlation part is not covered since there is no specific UCD for this.
The UCD-s accepted for each parameter are defined in `pyvo.mivot.glossary`.
The error classes are hard-coded as the most likely types.
- PErrorSym2D for 2D parameters
- PErrorSym1D for 1D parameters
Returns
-------
(dict, dict)
A mapping proposal for the EpochPosiion + errors that can be used as input parameter
by :py:meth:`pyvo.mivot.writer.InstancesFromModels.add_mango_epoch_position`.
"""
def _check_ucd(mapping_entry, ucd, mapping):
"""
Inner function checking that mapping_entry matches with ucd according to
`pyvo.mivot.glossary`
"""
if mapping_entry in mapping:
return False
dict_entry = getattr(EpochPositionAutoMapping, mapping_entry)
if isinstance(dict_entry, list):
return ucd in dict_entry
else:
return ucd.startswith(dict_entry)
def _check_obs_date(field):
""" check if the field can be interpreted as a value date time
This algorithm is a bit specific for Vizier CS
"""
xtype = field.xtype
unit = field.unit
representation = None
if xtype == "timestamp" or unit == "'Y:M:D'" or unit == "'Y-M-D'":
representation = "iso"
# let's assume that dates expressed as days are MJD
elif xtype == "mjd" or unit == "d":
representation = "mjd"
if representation is None and unit == "year":
representation = "year"
if representation is not None:
field_ref = field.ID if field.ID is not None else field.name
return {"dateTime": field_ref, "representation": representation}
return None
table = self._votable.get_first_table()
fields = table.fields
mapping = {}
error_mapping = {}
for field in fields:
ucd = field.ucd
for mapping_entry in Roles.EpochPosition:
if _check_ucd(mapping_entry, ucd, mapping) is True:
if mapping_entry == "obsDate":
if (obs_date_mapping := _check_obs_date(field)) is not None:
mapping[mapping_entry] = obs_date_mapping
else:
mapping[mapping_entry] = field.ID if field.ID is not None else field.name
# Once we got a parameter mapping, we look for its associated error
# This nested loop makes sure we never have error without value
for err_field in fields:
err_ucd = err_field.ucd
# We assume the error UCDs are the same the these of the
# related quantities but prefixed with "stat.error;" and without "meta.main" qualifier
if err_ucd == ("stat.error;" + ucd.replace(";meta.main", "")):
param_mapping = err_field.ID if err_field.ID is not None else err_field.name
if mapping_entry == "parallax":
error_mapping[mapping_entry] = {"class": "PErrorSym1D",
"sigma": param_mapping}
elif mapping_entry == "radialVelocity":
error_mapping[mapping_entry] = {"class": "PErrorSym1D",
"sigma": param_mapping}
elif mapping_entry == "longitude":
if "position" in error_mapping:
error_mapping["position"]["sigma1"] = param_mapping
else:
error_mapping["position"] = {"class": "PErrorSym2D",
"sigma1": param_mapping}
elif mapping_entry == "latitude":
if "position" in error_mapping:
error_mapping["position"]["sigma2"] = param_mapping
else:
error_mapping["position"] = {"class": "PErrorSym2D",
"sigma2": param_mapping}
elif mapping_entry == "pmLongitude":
if "properMotion" in error_mapping:
error_mapping["properMotion"]["sigma1"] = param_mapping
else:
error_mapping["properMotion"] = {"class": "PErrorSym2D",
"sigma1": param_mapping}
elif mapping_entry == "pmLatitude":
if "properMotion" in error_mapping:
error_mapping["properMotion"]["sigma2"] = param_mapping
else:
error_mapping["properMotion"] = {"class": "PErrorSym2D",
"sigma2": param_mapping}
return mapping, error_mapping