Rings

NavModelRings and NavModelRingsSimulated build ring feature models from configuration (or simulation parameters). The nav.nav_model.rings subpackage holds the domain model: validation, filtering, and rendering are separated so each concern can be tested in isolation.

Ring domain model

RingFeatureType is a two-value enum (RINGLET, GAP) that controls how the shaded region is oriented relative to the known edge when only one edge is present.

RingBaseOrbitMode and RingPerturbationMode are frozen dataclasses that hold the orbital parameters for an edge. RingPerturbationMode exposes an is_inclination_mode property (mode number > 90) that the rendering path uses to skip inclination modes that require a different backplane not yet supported.

RingEdgeData bundles the base orbit and zero or more perturbation modes for a single ring edge. Its base_radius and rms properties extract the canonical radius and uncertainty from the base orbit. parsed_modes_for_backplane() returns the list of (mode, amplitude, phase, pattern_speed) tuples that the oops backplane API consumes, omitting inclination modes.

RingFeature is the core domain object—a frozen dataclass that owns a single ring feature (ringlet or gap) with optional inner and outer edges. It is constructed via from_config(key, data) which validates the YAML dictionary immediately and raises ValueError on any malformed input. Derived cached fields (_start_et, _end_et) are computed in __post_init__ using object.__setattr__() because the dataclass is frozen. Query methods (is_visible_at, is_in_radius_range, uncertainty, all_base_radii, uses_fade_for_edge) allow the filter to make decisions without coupling to the rendering path. The render(context) method dispatches to either _render_full_ringlet() (for two-edge features) or _render_single_edge() (for one-edge features) and returns one or two RingRenderResult instances.

validate_no_date_overlaps(features) is a module-level function that enforces authoring invariants: if two features with explicit date ranges share overlapping radii, their date ranges must not overlap. This is checked as a hard error at YAML load time to catch mistakes before any rendering occurs.

Ring filtering pipeline

RingFeatureFilter applies a four-pass decision pipeline to a list of RingFeature objects and returns only the features that should be rendered:

Filter passes

Pass

Name

Decision

1

Date

Exclude the feature if the observation time is outside its [start_date, end_date) window. Features without explicit dates are always kept.

2

Radius

Exclude the feature if neither edge falls within [min_radius, max_radius]. Partially visible features (one edge in range) are kept so the renderer can handle the partial case.

3

Resolvability

For two-edge features (RINGLETs and GAPs where both edges are in the FOV), exclude the feature if the gap width outer.base_radius - inner.base_radius is smaller than min_feature_pixels * min_resolution along the feature. The motivation is that an unresolvable gap provides no useful navigational information—shading its surroundings without a visible gap would be misleading.

4

Fade conflict

For each edge that uses a fade, check whether a neighboring edge’s all_edge_radii would push the conflict-adjusted fade width below min_allowed_fade_width_pix * min_resolution at best resolution. If so, exclude that edge. A feature whose only remaining edge is excluded is dropped entirely. A feature retaining at least one edge is passed through with the excluded edge set to None (a trimmed feature).

The filter logs every exclusion decision at DEBUG level with the feature key, pass number, and reason, allowing detailed inspection without polluting INFO output. Exclusion and trimming are kept in the filter rather than in the renderer so that each concern can be tested independently.

Ring YAML configuration

Top-level ring model parameters

These keys sit directly under the rings: section (not under rings.ring_features.<PLANET>). They are shared across all planets and control post-render processing that is applied to every feature before the NavModelResult is constructed.

Parameter

Default

Description

remove_planet_shadow

true

When true, pixels of each rendered ring feature that fall inside the shadow of the nearest planet are zeroed out of the model image and removed from the model mask. The shadow boundary is computed once per observation via obs.ext_bp.where_inside_shadow(ring_target, planet.lower()), where ring_target is <planet>:ring (e.g. saturn:ring) and planet is obs.closest_planet. If the backplane call raises an exception (e.g. degenerate illumination geometry), a warning is logged and shadow removal is skipped for that observation. Removing shadowed pixels prevents the navigator from matching bright model arcs against the dark shadow region, which would introduce a systematic offset bias.

remove_body_shadows

false

Reserved for future implementation. When set, it will zero out ring model pixels that lie in the shadows cast by moons. The flag is accepted by the configuration parser but has no effect in the current release.

Shadow removal is applied after rendering and before NavModelResult construction, so the model_img and model_mask stored in each result already reflect the masking. At INFO log level the orchestrator reports the shadow pixel count:

Planet shadow removal: 1284 pixel(s) inside SATURN shadow will be masked

Set general.log_level_model_rings to DEBUG for the full backplane call trace.

Planetary ring features are defined in separate YAML files under src/nav/config_files/. The default Saturn configuration is in config_21_saturn_rings.yaml.

YAML structure

rings:
  ring_features:
    SATURN:                             # Planet name (must match obs.closest_planet)
      epoch: '2004-01-01 12:00:00'     # Reference epoch for precessing modes
      fade_width_pix: 100.0            # Nominal fade width in pixels for each edge
      min_allowed_fade_width_pix: 2.0  # Minimum fade width before edge is excluded
      min_feature_pixels: 2.0          # Minimum resolvable gap width (pass-3 filter)
      features:                        # Dict of named ring features
        colombo_gap:
          feature_type: GAP
          outer_data:                  # Edge data list (mode 1 = base orbit)
            - mode: 1
              a: 77870.0              # Semi-major axis in km
              ae: 100.0              # Amplitude of eccentricity (km)
              long_peri: 195.0        # Longitude of periapsis (degrees)
              rate_peri: 0.0          # Precession rate (degrees/day)
              rms: 2.0               # Edge uncertainty (km, 1-sigma RMS)
        titan_ringlet:
          feature_type: RINGLET
          start_date: '2004-01-01'   # Optional: feature active from this UTC date
          end_date: '2017-09-15'     # Optional: feature active until this UTC date (exclusive)
          inner_data:
            - mode: 1
              a: 77517.0
              ae: 3.0
              long_peri: 0.0
              rate_peri: 0.0
              rms: 1.0
          outer_data:
            - mode: 1
              a: 77871.0
              ae: 5.0
              long_peri: 0.0
              rate_peri: 0.0
              rms: 2.0
            - mode: 2                # Optional perturbation mode
              amplitude: 1.5
              phase: 30.0
              pattern_speed: 0.5

Planet-level parameters

Parameter

Description

epoch

Reference UTC date-time string for evaluating precessing orbital modes. All long_peri angles and rate_peri precession rates are evaluated relative to this epoch. Required.

fade_width_pix

Desired fade width in pixels for each rendered edge. The fade spans this many pixels everywhere in the image: at the ansae (high resolution) the fade covers fewer kilometres; near the ansa edges (low resolution) it covers more. Required; must be positive.

min_allowed_fade_width_pix

Minimum fade width in pixels. If a neighboring edge would force the conflict-adjusted fade below this threshold (at the best resolution along the edge), the edge is excluded by the filter. Required; must be positive.

min_feature_pixels

Minimum resolvable width in pixels for two-edge features (RINGLETs and GAPs where both edges fall within the FOV). Features narrower than min_feature_pixels * min_resolution are excluded by the filter because the gap cannot be detected. Required; must be positive.

Feature-level parameters

Parameter

Description

feature_type

GAP or RINGLET. Determines how single-edge features are shaded and how the region between a pair of edges is filled.

inner_data / outer_data

List of mode dicts describing the edge orbit. At least one of these must be present. Mode 1 is the base orbit (required in the list); higher modes are perturbations. See edge mode parameters below.

start_date

Optional UTC date string. The feature is active only for observations at or after this date.

end_date

Optional UTC date string. The feature is active only for observations strictly before this date.

Edge mode parameters (mode 1 — base orbit)

Field

Description

mode

Must be 1 for the base orbit entry.

a

Semi-major axis in km. Must be positive.

ae

Eccentricity amplitude in km (half of peak-to-peak radial variation).

long_peri

Longitude of periapsis at the reference epoch, in degrees.

rate_peri

Precession rate of periapsis, in degrees per day.

rms

Edge position uncertainty, in km (1-sigma RMS). Used for NavModelResult.uncertainty.

Edge mode parameters (mode > 1 — perturbation modes)

Field

Description

mode

Mode number. Values 2-90 are radial perturbations (supported). Values > 90 are inclination modes (stored but silently skipped during rendering because the current backplane API does not support them).

amplitude

Perturbation amplitude in km.

phase

Phase angle at the reference epoch, in degrees.

pattern_speed

Pattern speed in degrees per day.

Validation and loading

RingFeature.from_config() validates every feature dictionary immediately when it is read. Errors raise ValueError with the feature key in the message. Checks include:

  • feature_type must be "GAP" or "RINGLET".

  • At least one of inner_data / outer_data must be present.

  • Each mode list must contain exactly one mode-1 entry.

  • a must be positive; rms must be non-negative.

  • Date strings must be parseable by utc_to_et.

After all features are loaded, validate_no_date_overlaps() performs a cross-feature pass. If two features share overlapping radial extents and both have explicit [start_date, end_date) windows that overlap in time, a ValueError is raised. This catches authoring mistakes where a curator accidentally activates two conflicting features simultaneously.

Adding a new planet

To configure rings for a new planet (e.g., Uranus):

  1. Create src/nav/config_files/config_XX_uranus_rings.yaml with the structure shown above, replacing SATURN with URANUS.

  2. Add a !include directive (or equivalent) in the main config so that the new file is loaded alongside the Saturn file:

    rings:
      ring_features:
        !include config_XX_uranus_rings.yaml
    
  3. Populate fade_width_pix, min_allowed_fade_width_pix, min_feature_pixels, and epoch for the new planet.

  4. Add individual features under features: using the same format as Saturn.

No code changes are required. The orchestrator (NavModelRings) reads whichever planet name appears in obs.closest_planet and looks it up in rings.ring_features at runtime.