curryer.compute.geometry ======================== .. py:module:: curryer.compute.geometry .. autoapi-nested-parse:: Selective geometric data-field computation. This module computes the geolocation/geometry ancillary data fields that geolocated products commonly need. It is organized in two layers, both mission-generic: - **Math-only leaf functions** (``sc_radius``, ``colatitude``, ``subobserver_point``, ``earth_sun_distance``, ``satellite_altitude``) -- pure, vectorized, SPICE-free. They take high-level inputs (positions) as arguments and can be called directly to compose custom fields; the coordinate/frame primitives they build on (e.g. ``ecef_to_geodetic``) stay in :mod:`curryer.compute.spatial`. - **A selective-compute registry** -- a declarative map from each output field to the SPICE-derived inputs ("providers") it needs. A caller requests an arbitrary subset of fields and only the inputs that subset needs are queried, each provider exactly once, never per-field. This is the pre-built convenience over the leaf tools. Why a registry rather than one method per field: the field-to-input mapping is many-to-many and growing. The registry computes the minimal provider set for any requested subset automatically; the naive alternative (each field querying its own inputs) re-queries SPICE redundantly. Two public accessors are exposed on :class:`GeometryData`: - ``get_geometry(ugps_times, fields)`` -> :class:`pandas.DataFrame` -- the primary, tabular API. Vector fields expand to per-field-prefixed columns. - ``get_vectors(ugps_times, fields)`` -> ``{field: (N, k) ndarray}`` -- the typed sibling, addressed by field name rather than by string-built column prefixes. Mission genericity: the observing body is the only mission input (a constructor argument). Only universal identifiers (``EARTH``, ``SUN``, ``ITRF93``) appear in code -- no spacecraft/instrument names are hardcoded. Frame contract: the Earth-fixed (ECEF) fields all share a single reference frame -- the ``earth_frame`` configured on :class:`GeometryData` (``ITRF93`` by default) -- so fields are never combined in mismatched reference frames. Fill contract: SPICE coverage gaps (and off-Earth samples) surface as NaN. The providers query with ``allow_nans`` (True by default), so an uncovered time yields a NaN provider row; the math-only leaves are pure and propagate NaN elementwise, so every field is NaN on exactly the rows its inputs are missing and finite elsewhere -- per time, per field. Downstream maps those NaNs onto the product ``_FillValue`` (e.g. -999); the rows are never dropped or back-filled. Available fields: request any by name (``GeometryData.available_fields()`` lists them). Each field expands to the columns below. - ``sc_radius`` -> ``spacecraft_radius`` -- observer distance from Earth center. - ``subsatellite`` -> ``subsatellite_latitude``, ``subsatellite_longitude``, ``subsatellite_colatitude`` -- ground point beneath the spacecraft. - ``subsolar`` -> ``subsolar_latitude``, ``subsolar_longitude``, ``subsolar_colatitude`` -- ground point beneath the Sun. - ``earth_sun_distance`` -> ``earth_sun_distance`` -- Earth-Sun distance (AU). - ``sc_position`` -> ``spacecraft_position_x``, ``spacecraft_position_y``, ``spacecraft_position_z`` -- spacecraft position (ECEF). - ``sc_altitude`` -> ``spacecraft_altitude`` -- observer geodetic height above the ellipsoid (km). Attributes ---------- .. autoapisummary:: curryer.compute.geometry.logger Classes ------- .. autoapisummary:: curryer.compute.geometry.GeometryData Functions --------- .. autoapisummary:: curryer.compute.geometry.sc_radius curryer.compute.geometry.colatitude curryer.compute.geometry.subobserver_point curryer.compute.geometry.earth_sun_distance curryer.compute.geometry.satellite_altitude Module Contents --------------- .. py:data:: logger .. py:function:: sc_radius(observer_position: numpy.ndarray) -> numpy.ndarray Distance of the observer from the body center (vectorized). Implements the "radius of satellite from center of Earth" field. :param observer_position: Observer (e.g., spacecraft) positions in rectangular coordinates, shape (..., 3). Units are arbitrary; the result is in the input's units. :type observer_position: np.ndarray :returns: Euclidean distance ``||position||`` per point, shape (...,). :rtype: np.ndarray .. py:function:: colatitude(latitude: numpy.ndarray, degrees: bool = True) -> numpy.ndarray Convert geodetic latitude to colatitude (vectorized). Colatitude is the complement of latitude (``90 - lat``), ranging from 0 at the north pole to 180 at the south pole. Implements the surface-point colatitude field; also reused by the sub-point fields. :param latitude: Geodetic latitude. :type latitude: np.ndarray :param degrees: If True (default), inputs and outputs are in degrees, otherwise radians. :type degrees: bool, optional :returns: Colatitude in the same units as the input. :rtype: np.ndarray .. py:function:: subobserver_point(observer_position: numpy.ndarray, degrees: bool = True) -> numpy.ndarray Sub-observer geodetic latitude, longitude, and colatitude (vectorized). The sub-observer point is the geodetic ground point directly beneath the observer; it shares the observer's geodetic latitude and longitude. Generic over the observing body: pass the spacecraft position for the sub-satellite point or the Sun position for the sub-solar point. :param observer_position: Observer positions in ECEF rectangular coordinates (km), shape (..., 3). :type observer_position: np.ndarray :param degrees: If True (default), latitude/longitude/colatitude are in degrees, otherwise radians. Longitude follows ``ecef_to_geodetic`` (-180 to 180). :type degrees: bool, optional :returns: Stacked ``[latitude, longitude, colatitude]``, shape (..., 3). :rtype: np.ndarray .. py:function:: earth_sun_distance(earth_sun_position: numpy.ndarray, au: bool = True) -> numpy.ndarray Distance between Earth and Sun (vectorized). Implements the Earth-Sun distance field. The input is the Earth-to-Sun position vector (any frame, since only the magnitude is used). :param earth_sun_position: Earth-to-Sun position in rectangular coordinates (km), shape (..., 3). :type earth_sun_position: np.ndarray :param au: If True (default), return astronomical units, otherwise kilometers. :type au: bool, optional :returns: Earth-Sun distance per point, shape (...,). :rtype: np.ndarray .. py:function:: satellite_altitude(observer_position: numpy.ndarray) -> numpy.ndarray Geodetic altitude of the observer above the ellipsoid (vectorized). The height-above-ellipsoid component of the observer's geodetic position -- the piece ``subobserver_point`` drops when it returns latitude/longitude/colatitude. Complements ``sc_radius`` (the geocentric distance from the body center). :param observer_position: Observer (e.g., spacecraft) positions in ECEF rectangular coordinates (km), shape (..., 3). :type observer_position: np.ndarray :returns: Geodetic altitude in km, shape (...,). :rtype: np.ndarray .. py:class:: GeometryData(observer, microsecond_cadence=None, earth='EARTH', sun='SUN', earth_frame=None) Bases: :py:obj:`curryer.compute.abstract.AbstractMissionData` Selective geometric data-field server. Construct with the observing body, then request any subset of registered fields via :meth:`get_geometry` (DataFrame) or :meth:`get_vectors` (typed ``{field: ndarray}``). Only the SPICE inputs the requested subset needs are queried, each exactly once. Relevant kernels must already be loaded for the requested times, mirroring the other ``compute`` servers. The requested times are the independent variable: :meth:`get_geometry` and :meth:`get_vectors` evaluate every field at exactly the ``ugps_times`` passed -- an arbitrary, possibly non-uniform array -- with no resampling or interpolation. ``microsecond_cadence`` / :meth:`get_times` only offers an optional uniform grid for callers that want one; it never constrains which times are queried. :param observer: Observing body whose position is queried -- typically the instrument or spacecraft. :type observer: str or int or spicierpy.obj.Body :param microsecond_cadence: Default cadence for :meth:`get_times`, in microseconds. :type microsecond_cadence: int, optional :param earth: Central body and solar body for the ephemeris queries. Default ``"EARTH"`` / ``"SUN"``; overridable so no body name is fixed in code. :type earth: str or int or spicierpy.obj.Body, optional :param sun: Central body and solar body for the ephemeris queries. Default ``"EARTH"`` / ``"SUN"``; overridable so no body name is fixed in code. :type sun: str or int or spicierpy.obj.Body, optional :param earth_frame: Earth-fixed (ECEF) reference frame. Default ``spatial.EARTH_FRAME`` (ITRF93). :type earth_frame: str or spicierpy.obj.Frame, optional .. py:attribute:: DEFAULT_CADENCE :value: 60000000 .. py:attribute:: observer .. py:attribute:: earth :value: 'EARTH' .. py:attribute:: sun :value: 'SUN' .. py:attribute:: earth_frame :value: 'ITRF93' .. py:method:: available_fields() :classmethod: Tuple of registered field names. .. py:method:: get_geometry(ugps_times: numpy.ndarray, fields: list[str] | None = None) -> pandas.DataFrame Compute the requested fields as a table. :param ugps_times: One or more times in GPS microseconds. Arbitrary and need not be uniformly spaced; each time is evaluated exactly (no interpolation). :type ugps_times: array_like of int :param fields: Field names to compute. Default is the ephemeris-only set -- the fields computable from position/ephemeris providers alone. :type fields: list of str, optional :returns: One row per time (index ``ugps``); vector fields expand to per-field-prefixed columns (e.g. ``spacecraft_position_x, spacecraft_position_y, spacecraft_position_z``). Times outside SPICE coverage are NaN across that field's columns (see the module Fill contract). :rtype: pandas.DataFrame .. py:method:: get_vectors(ugps_times: numpy.ndarray, fields: list[str]) -> dict[str, numpy.ndarray] Compute the requested fields as typed arrays. The typed sibling of :meth:`get_geometry`, addressed by field name rather than by string-built column prefixes. :param ugps_times: One or more times in GPS microseconds. Arbitrary and need not be uniformly spaced; each time is evaluated exactly (no interpolation). :type ugps_times: array_like of int :param fields: Field names to compute. :type fields: list of str :returns: **dict of {str** -- Maps each field name to its ``(N, k)`` array. Vector fields (e.g. ``sc_position``) are ``(N, 3)`` in the configured Earth-fixed frame (``ITRF93`` by default). Rows outside SPICE coverage are NaN (see the module Fill contract). :rtype: numpy.ndarray}