Source code for nav.feature.flags

"""Sum-type flag dataclasses carried on NavFeature.flags.

Each feature type has a small dataclass listing the technique-specific
boolean / scalar flags relevant to that type.  Carrying them as a typed sum
type instead of a free-form ``dict[str, Any]`` lets static type checkers see
which fields exist for each type and lets curator code copy known fields by
attribute.
"""

from dataclasses import dataclass

__all__ = [
    'BodyBlobFlags',
    'BodyDiscFlags',
    'CartographicModelFlags',
    'LimbArcFlags',
    'NavFeatureFlags',
    'RingAnnulusFlags',
    'RingEdgeFlags',
    'StarFlags',
    'TerminatorArcFlags',
]


[docs] @dataclass(frozen=True) class StarFlags: """Flags carried on a STAR feature. Parameters: saturated: True if the detected star peak hit the camera's full-well DN; centroiding switches from peak-Gaussian-fit to annular brightness-weighted moment. smear_length_px: Expected smear length in pixels at this image's spacecraft attitude rate. Must be ``>= 0``. in_body_silhouette: True if the predicted star position falls inside a predicted body silhouette in extfov. in_saturation_or_cosmic_mask: True if the predicted star position falls inside a saturation or cosmic-ray mask pixel. predicted_snr: Magnitude-margin-derived effective SNR for the catalog star (``SNR_REF * 2.512 ** (mag_limit - vmag)``), not a DN-based photometric SNR. Monotone in catalog brightness, so ``StarUniqueMatchNav`` can still rank stars by it when picking the unique-bright pair. ``0.0`` for fixtures or features whose model did not populate it. Must be ``>= 0``. vmag: Catalog V-band magnitude of the star, or ``None`` when the catalog entry has no magnitude. Used by ``StarUniqueMatchNav`` to compute the magnitude margin to the next-brightest star. """ saturated: bool = False smear_length_px: float = 0.0 in_body_silhouette: bool = False in_saturation_or_cosmic_mask: bool = False predicted_snr: float = 0.0 vmag: float | None = None
[docs] def __post_init__(self) -> None: """Validate ``smear_length_px`` and ``predicted_snr`` are non-negative.""" if self.smear_length_px < 0.0: raise ValueError(f'smear_length_px must be >= 0; got {self.smear_length_px!r}') if self.predicted_snr < 0.0: raise ValueError(f'predicted_snr must be >= 0; got {self.predicted_snr!r}')
[docs] @dataclass(frozen=True) class LimbArcFlags: """Flags carried on a LIMB_ARC feature. Parameters: body_name: SPICE body name whose limb this arc traces. visible_arc_fraction: Fraction of total limb length inside extfov and not occluded ``[0, 1]``. """ body_name: str = '' visible_arc_fraction: float = 0.0
[docs] def __post_init__(self) -> None: """Validate ``visible_arc_fraction`` is in ``[0, 1]``.""" if not 0.0 <= self.visible_arc_fraction <= 1.0: raise ValueError( f'visible_arc_fraction must lie in [0, 1]; got {self.visible_arc_fraction!r}' )
[docs] @dataclass(frozen=True) class TerminatorArcFlags: """Flags carried on a TERMINATOR_ARC feature. Parameters: body_name: SPICE body name whose terminator this arc traces. visible_arc_fraction: Fraction of total terminator length inside extfov and lit ``[0, 1]``. phase_angle_factor: ``sin(phase_angle)`` factor used in reliability; peaks at 90-degree crescent. Must lie in ``[0, 1]``. """ body_name: str = '' visible_arc_fraction: float = 0.0 phase_angle_factor: float = 0.0
[docs] def __post_init__(self) -> None: """Validate fractions and the phase-angle factor.""" if not 0.0 <= self.visible_arc_fraction <= 1.0: raise ValueError( f'visible_arc_fraction must lie in [0, 1]; got {self.visible_arc_fraction!r}' ) if not 0.0 <= self.phase_angle_factor <= 1.0: raise ValueError( f'phase_angle_factor must lie in [0, 1]; got {self.phase_angle_factor!r}' )
[docs] @dataclass(frozen=True) class RingEdgeFlags: """Flags carried on a RING_EDGE feature. Parameters: is_straight_line: True when the polyline's deviation from a straight-line fit is below threshold. Triggers rank-1 covariance handling at the technique level. polarity_predictable: True only when the per-edge static-catalog entry guarantees the gradient direction across the edge in this scene. Default is False. edge_name: Name of the ring edge in the static catalog. planet_name: Planet whose rings this edge belongs to. """ is_straight_line: bool = False polarity_predictable: bool = False edge_name: str = '' planet_name: str = ''
[docs] @dataclass(frozen=True) class BodyDiscFlags: """Flags carried on a BODY_DISC feature. Parameters: body_name: SPICE body name whose disc this feature renders. overflow_fov_fraction: Fraction of the disc area outside the sensor ``[0, 1]``. Same value as ``BodyDiscGeometry.overflow_fraction``; duplicated here for type-specific access. """ body_name: str = '' overflow_fov_fraction: float = 0.0
[docs] def __post_init__(self) -> None: """Validate ``overflow_fov_fraction`` is in ``[0, 1]``.""" if not 0.0 <= self.overflow_fov_fraction <= 1.0: raise ValueError( f'overflow_fov_fraction must lie in [0, 1]; got {self.overflow_fov_fraction!r}' )
[docs] @dataclass(frozen=True) class BodyBlobFlags: """Flags carried on a BODY_BLOB feature. Parameters: body_name: SPICE body name whose blob this feature represents. predicted_diameter_px: Predicted disc diameter in pixels (longer axis of the predicted ellipse silhouette). Must be ``>= 0``. phase_angle_deg: Phase angle (Sun -> body -> observer) at the body's center, in degrees. Recorded for diagnostic inspection; the BLOB confidence formula consumes ``phase_irregularity_factor`` instead, since raw phase alone understates the centroid uncertainty for an irregular body. Must be in ``[0, 180]``. phase_irregularity_factor: Dimensionless coupling of phase angle and shape irregularity, computed by the body NavModel as ``(ellipsoid_rms_residual_km / body_radius_km) * (1 + 2 * sin^2(phase / 2))``. Captures the centroid-bias risk that the lit-weighted predicted centroid cannot fully correct for. The fractional ``residual / radius`` term is ~ 0.005 for regular moons and ~ 0.05-0.10 for irregular satellites; the phase factor goes from 1 at full-phase (rotational orientation always unknown) to 3 at full crescent (most of the body unlit, hiding most of the irregularity). Must be ``>= 0``. sub_solar_dir_vu: Unit ``(v, u)`` image-plane direction from the body's geometric center toward the bright limb (the projection of the body-to-Sun vector). ``(0.0, 0.0)`` when the direction is unknown or undefined (a near-full-phase body whose lit centroid coincides with its geometric center). ``BodyBlobNav`` orients its phase-aware coarse-acquisition template along this direction so a high-phase crescent displaced beyond its predicted bounding box is still found; a filled-disc template cannot match a thin crescent. Both components must lie in ``[-1, 1]``. """ body_name: str = '' predicted_diameter_px: float = 0.0 phase_angle_deg: float = 0.0 phase_irregularity_factor: float = 0.0 sub_solar_dir_vu: tuple[float, float] = (0.0, 0.0)
[docs] def __post_init__(self) -> None: """Validate per-field constraints.""" if self.predicted_diameter_px < 0.0: raise ValueError( f'predicted_diameter_px must be >= 0; got {self.predicted_diameter_px!r}' ) if not (0.0 <= self.phase_angle_deg <= 180.0): raise ValueError(f'phase_angle_deg must be in [0, 180]; got {self.phase_angle_deg!r}') if self.phase_irregularity_factor < 0.0: raise ValueError( f'phase_irregularity_factor must be >= 0; got {self.phase_irregularity_factor!r}' ) dir_v, dir_u = self.sub_solar_dir_vu if not (-1.0 <= dir_v <= 1.0 and -1.0 <= dir_u <= 1.0): raise ValueError( f'sub_solar_dir_vu components must lie in [-1, 1]; got {self.sub_solar_dir_vu!r}' )
[docs] @dataclass(frozen=True) class RingAnnulusFlags: """Flags carried on a RING_ANNULUS feature. Parameters: planet_name: Planet whose ring system this annulus represents. constituent_edge_count: Number of catalog edges fused into this annulus template. Must be a non-negative integer. """ planet_name: str = '' constituent_edge_count: int = 0
[docs] def __post_init__(self) -> None: """Validate ``constituent_edge_count`` is a non-negative integer.""" if not isinstance(self.constituent_edge_count, int) or isinstance( self.constituent_edge_count, bool ): raise TypeError( f'constituent_edge_count must be int; got ' f'{type(self.constituent_edge_count).__name__}' ) if self.constituent_edge_count < 0: raise ValueError( f'constituent_edge_count must be >= 0; got {self.constituent_edge_count!r}' )
[docs] @dataclass(frozen=True) class CartographicModelFlags: """Flags carried on a CARTOGRAPHIC_MODEL feature. Parameters: body_name: SPICE body name the cartographic mosaic represents. mosaic_source: Identifier of the mosaic file (e.g. file basename or URL). """ body_name: str = '' mosaic_source: str = ''
NavFeatureFlags = ( StarFlags | LimbArcFlags | TerminatorArcFlags | RingEdgeFlags | BodyDiscFlags | BodyBlobFlags | RingAnnulusFlags | CartographicModelFlags ) """Sum type spanning every NavFeatureType's flag dataclass."""