"""Planetary coordinate reference systems for craterpy.
Standard planetocentric/planetographic CRS come from :func:`planetarypy.crs.body_crs`.
This module adds the cases planetarypy does not produce: Vesta Claudia frames and
the west-positive planetographic for bodies with no IAU ographic code (see
``_SPECIAL_CRS``).
``body_crs`` is always called with an integer NAIF id (``_BODY_NAIF``), never a
name, to avoid planetarypy's name-resolution path importing
``planetarypy.constants`` (which can trigger a one-time archive download).
"""
from planetarypy.crs import body_crs
from pyproj import CRS
from pyproj.exceptions import CRSError
# Body name -> NAIF id. The IAU CRS code is naif_id * 100 + variant offset
# (offset 0 = planetocentric, 1 = planetographic where IAU defines it).
_BODY_NAIF = {
"mercury": 199,
"venus": 299,
"moon": 301,
"earth": 399,
"mars": 499,
"ceres": 2000001,
"vesta": 2000004,
"europa": 502,
"ganymede": 503,
"callisto": 504,
"enceladus": 602,
"tethys": 603,
"dione": 604,
"rhea": 605,
"iapetus": 608,
"pluto": 999,
}
# Default CRS for each body, else assume planetocentric.
DEFAULT_CRS = {
"ceres": "planetographic",
"ganymede": "planetographic",
"enceladus": "planetographic",
"tethys": "planetographic",
"dione": "planetographic",
"rhea": "planetographic",
"iapetus": "planetographic",
"mercury": "planetographic",
}
ALL_BODIES = list(_BODY_NAIF)
# craterpy alias -> planetarypy `body_crs` system name.
_SYSTEM_MAP = {"planetocentric": "ocentric", "planetographic": "ographic"}
# Bodies with no IAU planetographic code: craterpy fabricates one by flipping
# the planetocentric CRS to a west-positive (wnu) axis order.
_WNU_PLANETOGRAPHIC = {"mercury", "enceladus", "tethys", "dione", "rhea"}
def _special_crs(body: str, system: str, naif: int) -> CRS | None:
"""Return a craterpy-specific CRS not produced by planetarypy, else None."""
if body == "vesta":
if system == "claudia_dp":
# Claudia double prime == the IAU_2015 standard for Vesta.
return body_crs(naif, "ocentric")
if system == "claudia_p":
# Claudia Prime, Claudia crater at 136 E (10 deg W offset).
return CRS.from_proj4("+proj=longlat +R=255000 +lon_0=+10 +no_defs")
if system == "dawn_claudia":
# Dawn Claudia, Claudia crater at 356 E (210 deg E offset).
return CRS.from_proj4("+proj=longlat +R=255000 +lon_0=-210 +no_defs")
if system == "planetographic" and body in _WNU_PLANETOGRAPHIC:
return CRS.from_proj4(body_crs(naif, "ocentric").to_proj4() + " +axis=wnu")
return None
[docs]def get_crs(body: str, system: str | CRS = "default") -> CRS:
"""Retrieve a CRS for a body, or pass through any valid pyproj CRS.
Parameters
----------
body : str
Planetary body name (case-insensitive), e.g. 'Moon', 'mars', 'Vesta'.
system : str or pyproj.CRS
A system alias ('planetocentric', 'planetographic', 'default', or a
body-specific alias like 'claudia_dp'), or any valid pyproj CRS object
or string (e.g. an EPSG code), which is returned as-is.
"""
# Pass through any input that is already a valid CRS (object or string).
try:
return CRS.from_user_input(system)
except CRSError:
pass
body = body.lower()
system = system.lower()
if body not in _BODY_NAIF:
raise ValueError(
f"Body '{body}' is not supported. Choose one of {ALL_BODIES} "
"or open a feature request."
)
if system == "default":
system = DEFAULT_CRS.get(body, "planetocentric")
naif = _BODY_NAIF[body]
special = _special_crs(body, system, naif)
if special is not None:
return special
if system not in _SYSTEM_MAP:
raise ValueError(
f"Unknown Planetary body and system combo: '{body}', '{system}'."
)
try:
# Delegate the standard ocentric/ographic CRS to planetarypy. Raises
# for bodies that define no planetographic IAU code (e.g. pluto).
return body_crs(naif, _SYSTEM_MAP[system])
except ValueError as err:
raise ValueError(
f"Unknown Planetary body and system combo: '{body}', '{system}'."
) from err