curryer.compute.geometry

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 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 GeometryData:

  • get_geometry(ugps_times, fields) -> 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 GeometryData (ITRF93 by default) – so fields are never combined in mismatched reference frames. The *_inertial fields likewise share inertial_frame (J2000 by default), and satellite_attitude targets attitude_frame (defaulting to inertial_frame). A field’s frame is named in its description; the Earth-fixed and inertial state vectors are distinct fields, never the same field re-framed.

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

  • boresight -> boresight_x, boresight_y, boresight_z – instrument boresight unit vector (ECEF).

  • surface_colatitude -> surface_colatitude – colatitude of the boresight ellipsoid intersection.

  • viewing_zenith / solar_zenith -> same-named columns – geodetic zenith of the satellite / Sun at the boresight ellipsoid intersection.

  • viewing_azimuth / solar_azimuth -> same-named columns – satellite / Sun azimuth at that intersection.

  • relative_azimuth -> relative_azimuth – viewing relative to solar azimuth.

  • cone_angle -> cone_angle – boresight angle off the satellite-to-geocenter vector.

  • cone_angle_rate -> cone_angle_rate – finite-difference time derivative of cone_angle (deg/s) over the requested times.

  • sc_velocity -> spacecraft_velocity_x/y/z – observer velocity (ECEF, km/s).

  • satellite_attitude -> attitude_q0..q3 – observer body attitude quaternion (body -> attitude_frame, scalar-first).

  • clock_angle / clock_angle_rate -> same-named columns – boresight azimuth in the inertial-velocity orbital frame (CERES SCI-12) and its unwrapped rate (deg/s).

  • along_track_angle / cross_track_angle -> same-named columns – boresight look angles from nadir in the velocity-nadir and cross-track-nadir planes.

  • sc_position_inertial / sc_velocity_inertial -> spacecraft_position_inertial_x/y/z / spacecraft_velocity_inertial_x/y/z – observer state in inertial_frame (km, km/s).

  • boresight_inertial -> boresight_inertial_x/y/z – instrument boresight unit vector in inertial_frame.

  • moon_direction -> moon_direction_x/y/z – unit vector toward the Moon, in the instrument’s FOV frame.

  • moon_angular_radius -> moon_angular_radius – apparent angular radius of the lunar disk.

  • moon_distance -> moon_distance – apparent observer-Moon distance (km).

Angle convention: the surface angles are in degrees, over the boresight ellipsoid intersection. Azimuths (viewing_azimuth, solar_azimuth) are clockwise from geodetic North in [0, 360); zeniths (viewing_zenith, solar_zenith) are geodetic, from the local surface normal, in [0, 180]. relative_azimuth uses the CERES BDS R3V4 origin – mod(viewing_azimuth - solar_azimuth + 180, 360), so the Sun sits at 180 – and is the lossless unfolded value; the CERES [0, 180] fold (min(raa, 360 - raa)) is a separate, lossy, downstream step. cone_angle is in [0, 90] for Earth-disk views.

The lunar fields are a direction rather than surface angles. moon_direction_x/y/z is the unit vector from the observer to the Moon in the frame the instrument kernel declares its FOV in (INS<id>_FOV_FRAME), which is the frame the boresight and FOV boundary vectors already live in – so the boresight separation is a dot product against the boresight from curryer.spicierpy.ext.instrument_fov, with no rotation in between. A caller wanting a boresight-relative azimuth/elevation pair passes those same two vectors to curryer.compute.spatial.boresight_offset_angles, which takes the azimuth origin from the IK’s own FOV_REF_VECTOR.

The direction is to the center of the Moon, not its limb: the nearest edge of the disk sits moon_angular_radius closer than the center direction implies. The target is the Moon body center (NAIF 301), not the Earth-Moon barycenter, and the angular radius uses the largest of the body’s three radii, so the disk circumscribes rather than understates. The Moon is queried as an apparent (LT+S) position, so moon_distance is the light-time corrected range.

Attributes

Classes

GeometryData

Selective geometric data-field server.

Functions

sc_radius(→ numpy.ndarray)

Distance of the observer from the body center (vectorized).

colatitude(→ numpy.ndarray)

Convert geodetic latitude to colatitude (vectorized).

subobserver_point(→ numpy.ndarray)

Sub-observer geodetic latitude, longitude, and colatitude (vectorized).

earth_sun_distance(→ numpy.ndarray)

Distance between Earth and Sun (vectorized).

satellite_altitude(→ numpy.ndarray)

Geodetic altitude of the observer above the ellipsoid (vectorized).

Module Contents

curryer.compute.geometry.logger
curryer.compute.geometry.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.

Parameters:

observer_position (np.ndarray) – Observer (e.g., spacecraft) positions in rectangular coordinates, shape (…, 3). Units are arbitrary; the result is in the input’s units.

Returns:

Euclidean distance ||position|| per point, shape (…,).

Return type:

np.ndarray

curryer.compute.geometry.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.

Parameters:
  • latitude (np.ndarray) – Geodetic latitude.

  • degrees (bool, optional) – If True (default), inputs and outputs are in degrees, otherwise radians.

Returns:

Colatitude in the same units as the input.

Return type:

np.ndarray

curryer.compute.geometry.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.

Parameters:
  • observer_position (np.ndarray) – Observer positions in ECEF rectangular coordinates (km), shape (…, 3).

  • degrees (bool, optional) – If True (default), latitude/longitude/colatitude are in degrees, otherwise radians. Longitude follows ecef_to_geodetic (-180 to 180).

Returns:

Stacked [latitude, longitude, colatitude], shape (…, 3).

Return type:

np.ndarray

curryer.compute.geometry.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).

Parameters:
  • earth_sun_position (np.ndarray) – Earth-to-Sun position in rectangular coordinates (km), shape (…, 3).

  • au (bool, optional) – If True (default), return astronomical units, otherwise kilometers.

Returns:

Earth-Sun distance per point, shape (…,).

Return type:

np.ndarray

curryer.compute.geometry.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).

Parameters:

observer_position (np.ndarray) – Observer (e.g., spacecraft) positions in ECEF rectangular coordinates (km), shape (…, 3).

Returns:

Geodetic altitude in km, shape (…,).

Return type:

np.ndarray

class curryer.compute.geometry.GeometryData(observer, microsecond_cadence=None, earth='EARTH', sun='SUN', moon='MOON', earth_frame=None, inertial_frame='J2000', attitude_frame=None)

Bases: curryer.compute.abstract.AbstractMissionData

Selective geometric data-field server.

Construct with the observing body, then request any subset of registered fields via get_geometry() (DataFrame) or 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: get_geometry() and get_vectors() evaluate every field at exactly the ugps_times passed – an arbitrary, possibly non-uniform array – with no resampling or interpolation. microsecond_cadence / get_times() only offers an optional uniform grid for callers that want one; it never constrains which times are queried.

Scope – this server produces per-observation ancillary geometry: one row per requested time. Its fields are of two kinds: spacecraft-level quantities that are independent of pointing (subsatellite/subsolar points, radius, altitude, Earth-Sun distance) and angles referenced to the instrument’s single boresight – the IK boresight (viewing/solar zenith and azimuth, relative azimuth, cone angle, surface colatitude). It deliberately does not compute per-pixel geometry: an instrument with many pointing vectors (a camera focal plane, an offset pixel) is a (time, pixel) problem served by compute_ellipsoid_intersection() (custom_pointing_vectors=), whose vectorized ray-cast and per-pixel quality flags belong with the geolocated product rather than this ancillary table. A future mission needing a single non-boresight reference vector here (still one vector, not the array) is a natural additive extension, not a change to this contract.

Parameters:
  • observer (str or int or spicierpy.obj.Body) – Observing body whose position is queried – typically the instrument or spacecraft.

  • microsecond_cadence (int, optional) – Default cadence for get_times(), in microseconds.

  • earth (str or int or spicierpy.obj.Body, optional) – Central body, solar body and lunar body for the ephemeris queries. Default "EARTH" / "SUN" / "MOON"; overridable so no body name is fixed in code.

  • sun (str or int or spicierpy.obj.Body, optional) – Central body, solar body and lunar body for the ephemeris queries. Default "EARTH" / "SUN" / "MOON"; overridable so no body name is fixed in code.

  • moon (str or int or spicierpy.obj.Body, optional) – Central body, solar body and lunar body for the ephemeris queries. Default "EARTH" / "SUN" / "MOON"; overridable so no body name is fixed in code.

  • earth_frame (str or spicierpy.obj.Frame, optional) – Earth-fixed (ECEF) reference frame. Default spatial.EARTH_FRAME (ITRF93).

  • inertial_frame (str or spicierpy.obj.Frame, optional) – Inertial reference frame for the inertial state and the orbital-frame fields. Default "J2000"; overridable so no frame is fixed in code.

  • attitude_frame (str or spicierpy.obj.Frame, optional) – Target frame of the satellite_attitude quaternion. Defaults to inertial_frame. It is a separate knob because a product may reference its attitude to a frame other than the one its state vectors use – e.g. an Earth-fixed attitude quaternion alongside inertial position/velocity.

DEFAULT_CADENCE = 60000000
observer
earth = 'EARTH'
sun = 'SUN'
moon = 'MOON'
earth_frame = 'ITRF93'
inertial_frame = 'J2000'
attitude_frame = 'J2000'
classmethod available_fields()

Registered fields as GeometryField members.

Members are plain strings, so the result is usable directly as fields= selectors and each member carries its columns and description.

get_geometry(ugps_times: numpy.ndarray, fields: list[str] | None = None) → pandas.DataFrame

Compute the requested fields as a table.

Parameters:
  • ugps_times (array_like of int) – One or more times in GPS microseconds. Arbitrary and need not be uniformly spaced; each time is evaluated exactly (no interpolation).

  • fields (list of str, optional) – Field names to compute. Default is the ephemeris-only set (valid for any observer); attitude/instrument fields (e.g. boresight) must be requested explicitly. See available_fields() for the full list.

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

Return type:

pandas.DataFrame

get_vectors(ugps_times: numpy.ndarray, fields: list[str]) → dict[str, numpy.ndarray]

Compute the requested fields as typed arrays.

The typed sibling of get_geometry(), addressed by field name rather than by string-built column prefixes.

Parameters:
  • ugps_times (array_like of int) – One or more times in GPS microseconds. Arbitrary and need not be uniformly spaced; each time is evaluated exactly (no interpolation).

  • fields (list of str) – Field names to compute.

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

Return type:

numpy.ndarray}