Skip to content

Commit

Permalink
Merge pull request #591 from lmichel/feature-skycoord
Browse files Browse the repository at this point in the history
Feature skycoord
  • Loading branch information
bsipocz authored Oct 1, 2024
2 parents 18b0f1a + ff0eeea commit 9d3c5be
Show file tree
Hide file tree
Showing 18 changed files with 514 additions and 58 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Enhancements and Fixes

- Tables returned by RegistryResource.get_tables() now have a utype
attribute [#576]

- MIVOT module: If the MIVOT annotation block contains a valid instance of the ``mango:EpochPosition`` class,
the dynamic object describing the mapped data can generate a valid SkyCoord instance. [#591]


- Registry Spatial constraint now supports Astropy Quantities for the
radius argument [#594]
Expand Down
Binary file modified 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.
21 changes: 21 additions & 0 deletions docs/mivot/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,27 @@ with the `astropy.io.votable` API:
In this case, it is up to the user to ensure that the read data rows are those mapped by the Mivot annotations.
Get a SkyCoord Instance Directly From the Annotations
-----------------------------------------------------
Once you get a ``MivotInstance`` representing the last row read, you can use it to create an ``astropy.SkyCoord`` object.
.. code-block:: python
:caption: Accessing the model view of Astropy table rows
from pyvo.mivot import MivotViewer
m_viewer = MivotViewer(path_to_votable)
mivot_instance = m_viewer.dm_instance
print(mivot_instance.get_SkyCoord())
<SkyCoord (ICRS): (ra, dec) in deg(52.26722684, 59.94033461)
(pm_ra_cosdec, pm_dec) in mas / yr(-0.82, -1.85)>
This feature works under the condition that the annotations contain a valid instance of ``mango:EPochPosition``, otherwise
a ``NoMatchingDMTypeError`` is thrown.
Although not a standard at the time of writing, the class structure supported by this implementation must match the figure above.
For XML Hackers
---------------
Expand Down
187 changes: 187 additions & 0 deletions pyvo/mivot/features/sky_coord_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Utility transforming MIVOT annotation into SkyCoord instances
"""

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 = "epoch"
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'}


class SkyCoordBuilder(object):
'''
Utility generating SkyCoord instances from MIVOT annotations
- SkyCoord instances can only be built from model classes containing the minimal
set of required parameters (a position).
- In this implementation, only the mango:EpochPosition class is supported since
it contains the information required to compute the epoch propagation which is a major use-case
'''

def __init__(self, mivot_instance_dict):
'''
Constructor
parameters
-----------
mivot_instance_dict: viewer.MivotInstance.to_dict()
Internal dictionary of the dynamic Python object generated from the MIVOT block
'''
self._mivot_instance_dict = mivot_instance_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
This is a public method which could be extended to support other dmtypes.
returns
-------
SkyCoord
Instance built by the method
raises
------
NoMatchingDMTypeError
if the SkyCoord instance cannot be built.
"""
if 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):
"""
Format a date expressed in year as [scale]year
parameters
----------
hk_field: dict
MIVOT instance attribute
besselian: boolean
besselian time scale is used if True, otherwise Julian (default)
returns
-------
string
attribute value formatted as [scale]year
"""
scale = "J" if not besselian else "B"
return (f"{scale}{hk_field['value']}" if hk_field["unit"] in ("yr", "year")
else hk_field["value"])

def _get_space_frame(self, obstime=None):
"""
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
Astropy space frame instance
"""
coo_sys = self._mivot_instance_dict["coordSys"]
equinox = None
frame = coo_sys["spaceRefFrame"]["value"].lower()

if frame == 'fk4':
self._map_coord_names = skycoord_param_default
if "equinox" in coo_sys:
equinox = self._set_year_time_format(coo_sys["equinox"], True)
return FK4(equinox=equinox, obstime=obstime)
return FK4()

if frame == 'fk5':
self._map_coord_names = skycoord_param_default
if "equinox" in coo_sys:
equinox = self._set_year_time_format(coo_sys["equinox"])
return FK5(equinox=equinox)
return FK5()

if frame == 'galactic':
self._map_coord_names = skycoord_param_galactic
return Galactic()

self._map_coord_names = skycoord_param_default
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.
- The epoch (obstime) is meant to be given in year.
- ICRS frame is taken by default
- The cos-delta correction is meant to be applied.
The case ``mango:pmCosDeltApplied = False`` is not supported yet
returns
-------
SkyCoord
instance built by the method
"""
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:
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)
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"]
return SkyCoord(**kwargs)
6 changes: 3 additions & 3 deletions pyvo/mivot/features/static_reference_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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.exceptions import MivotError
from pyvo.mivot.utils.xpath_utils import XPath
from pyvo.utils.prototype import prototype_feature

Expand Down Expand Up @@ -33,7 +33,7 @@ def resolve(annotation_seeker, templates_ref, mivot_block):
The number of references resolved.
Raises
------
MappingException
MappingError
If the reference cannot be resolved.
NotImplementedError
If the reference is dynamic.
Expand All @@ -50,7 +50,7 @@ def resolve(annotation_seeker, templates_ref, mivot_block):
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}")
raise MivotError(f"Cannot resolve reference={dmref}")
# Resolve static references recursively
if not found_in_global:
StaticReferenceResolver.resolve(annotation_seeker, templates_ref, ele)
Expand Down
10 changes: 5 additions & 5 deletions pyvo/mivot/seekers/annotation_seeker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Utilities for extracting sub-blocks from a MIVOT mapping block.
"""
import logging
from pyvo.mivot.utils.exceptions import MivotException, MappingException
from pyvo.mivot.utils.exceptions import MivotError, MappingError
from pyvo.mivot.utils.vocabulary import Att, Ele
from pyvo.mivot.utils.vocabulary import Constant
from pyvo.mivot.utils.xpath_utils import XPath
Expand Down Expand Up @@ -73,7 +73,7 @@ def _find_templates_blocks(self):
logging.debug("Found " + Ele.TEMPLATES + " without " + Att.tableref)
self._templates_blocks["DEFAULT"] = child
else:
raise MivotException(Ele.TEMPLATES + " without " + Att.tableref + " must be unique")
raise MivotError(Ele.TEMPLATES + " without " + Att.tableref + " must be unique")

def _rename_ref_and_join(self):
"""
Expand Down Expand Up @@ -391,7 +391,7 @@ def get_collection_item_by_primarykey(self, coll_dmid, key_value):
Raises
------
MivotElementNotFound: If no element matches the criteria.
MappingException: If more than one element matches the criteria.
MappingError: If more than one element matches the criteria.
"""
eset = XPath.x_path(self._globals_block, ".//" + Ele.COLLECTION + "[@" + Att.dmid + "='"
+ coll_dmid + "']/" + Ele.INSTANCE + "/" + Att.primarykey
Expand All @@ -400,14 +400,14 @@ def get_collection_item_by_primarykey(self, coll_dmid, key_value):
message = (f"{Ele.INSTANCE} with {Att.primarykey} = {key_value} in "
f"{Ele.COLLECTION} {Att.dmid} {key_value} not found"
)
raise MivotException(message)
raise MivotError(message)
if len(eset) > 1:
message = (
f"More than one {Ele.INSTANCE} with {Att.primarykey}"
f" = {key_value} found in {Ele.COLLECTION} "
f"{Att.dmid} {key_value}"
)
raise MappingException(message)
raise MappingError(message)
logging.debug(Ele.INSTANCE + " with " + Att.primarykey + "=%s found in "
+ Ele.COLLECTION + " "
+ Att.dmid + "=%s", key_value, coll_dmid)
Expand Down
2 changes: 0 additions & 2 deletions pyvo/mivot/tests/test_mivot_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
"dmtype": "RealQuantity",
"value": 52.2340018,
"unit": "deg",
"astropy_unit": {},
"ref": "RAICRS"
},
"latitude": {
"dmtype": "RealQuantity",
"value": 59.8937333,
"unit": "deg",
"astropy_unit": {},
"ref": "DEICRS"
}
}
Expand Down
6 changes: 3 additions & 3 deletions pyvo/mivot/tests/test_mivot_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from astropy.utils.data import get_pkg_data_filename
from pyvo.mivot.utils.vocabulary import Constant
from pyvo.mivot.utils.dict_utils import DictUtils
from pyvo.mivot.utils.exceptions import MappingException
from pyvo.mivot.utils.exceptions import MappingError
from pyvo.mivot.version_checker import check_astropy_version
from pyvo.mivot import MivotViewer
from astropy import version as astropy_version
Expand Down Expand Up @@ -84,9 +84,9 @@ def test_no_mivot(path_no_mivot):
assert m_viewer.get_globals_models() is None

assert m_viewer.get_templates_models() is None
with pytest.raises(MappingException):
with pytest.raises(MappingError):
m_viewer._connect_table('_PKTable')
with pytest.raises(MappingException):
with pytest.raises(MappingError):
m_viewer._connect_table()

assert m_viewer.next_table_row() is None
Expand Down
Loading

0 comments on commit 9d3c5be

Please sign in to comment.