Observations

Overview

The nav.obs subsystem wraps an oops snapshot in a navigation-aware class that adds backplane caching, extended-FOV accessors, image masks, and per-instrument calibration hooks. Every navigation pipeline takes an ObsSnapshotInst instance as its input and reads the per-image data, geometry, and instrument-specific calibration through that object.

The class hierarchy splits responsibility across three axes:

  • Obs — abstract observation root, wires NavBase into the oops class tree so every concrete observation inherits config and logger.

  • ObsSnapshot — extends Obs and oops.observation.snapshot.Snapshot. Adds the FOV / extended-FOV accessors, backplane caching, and the per-image mask helpers that every navigation model and technique consumes.

  • ObsInst — abstract mix-in carrying per-instrument calibration: the from_file constructor contract, the optical PSF, the per-instrument visual-magnitude window, and the per-image public-metadata projection.

  • ObsSnapshotInst — concrete mix-in of ObsSnapshot and ObsInst. Per-mission subclasses derive from this base.

ObsSnapshot

ObsSnapshot is the navigation-side wrapper around an oops snapshot. It exposes three families of helpers:

Backplane caching and thread safety

Backplanes are cached in the underlying oops snapshot, so repeated queries of the same backplane on the same ObsSnapshot reuse the prior computation. The cache is mutating state attached to the snapshot itself: every call to backplane() (or to any helper that builds an oops.Backplane from the snapshot, including reproject() and create_cartographic_model()) allocates per-quantity arrays inside the snapshot’s cache and reads back any entries already present.

A single ObsSnapshot is therefore not safe for concurrent use across threads. Two threads that simultaneously sample backplanes through the same snapshot can race on the cache and return inconsistent or partially-populated arrays. Code that needs to parallelise over a single image must give each thread its own from_file() -constructed snapshot instance; the navigation pipeline runs serially per image, so the single-threaded contract is sufficient for the orchestrator’s own use.

ObsInst

ObsInst is the per-instrument calibration mix-in. It defines the abstract contract every per-mission subclass must implement:

  • from_file() — load an image file and return the matching ObsSnapshotInst. Subclasses delegate the actual decode to oops.hosts.<mission>.<inst>.from_file, then wrap the resulting oops snapshot.

  • star_psf() — returns the per-instrument optical PSF (typically a GaussianPSF). Used by NavModelStars to predict the per-star detection footprint.

  • star_psf_size() — returns the per-star kernel support rectangle in pixels.

  • star_min_usable_vmag() / star_max_usable_vmag() — the per-instrument photometric window. Stars outside this window do not contribute predicted detections.

  • get_public_metadata() — returns a JSON-friendly dict of per-image metadata fields (mission, instrument, exposure, filter wheel positions, etc.) for the per-image sidecar.

The inst_config property exposes the per-instrument YAML block loaded from src/nav/config_files/config_4N0_inst_*.yaml so subclass methods can read instrument-specific knobs without hard-coding them.

Per-instrument subclasses

Concrete subclasses live in src/nav/obs/ and are registered (via the nav.obs package’s __init__.py) under a per-mission / per-instrument key consumed by DataSet. Shipping subclasses:

  • ObsCassiniISS — Cassini ISS NAC and WAC. Delegates to oops.hosts.cassini.iss.from_file.

  • ObsVoyagerISS — Voyager 1 / 2 ISS NA and WA cameras. Delegates to oops.hosts.voyager.iss.from_file.

  • ObsGalileoSSI — Galileo SSI (uses full_fov=True to read the full sensor regardless of the on-chip ROI). Delegates to oops.hosts.galileo.ssi.from_file.

  • ObsNewHorizonsLORRI — New Horizons LORRI (passes calibration=False so the raw pixel values pass through). Delegates to oops.hosts.newhorizons.lorri.from_file.

  • ObsSim — simulated-image observation backed by a description of bodies and stars, consumed by nav_create_simulated_image and the simulated-image GUI driver.

Each subclass overrides from_file() to pull the right oops host, wires up the per-instrument PSF and photometric window, and forwards per-image metadata into get_public_metadata().

Simulated-image instrument config: inherit / override / self-specify

A simulated scene names an instrument (coiss_nac, vgiss, …) so its rendered frame and its ObsSim go through the same per-instrument units, noise, saturation, and PSF the navigator applies to a real frame. resolve_sim_inst_config() maps that name to the matching config_4N0_inst_*.yaml block (or the standalone sim block for the generic alias).

A scene may additionally carry an instrument_config mapping that is deep-merged over the resolved block (nested mappings merge key-by-key; scalars and lists replace). This gives three modes:

  • Inherit – omit instrument_config; every physical parameter tracks the named instrument’s config.

  • Override – supply only the keys to change; those are pinned to the scene and the rest still track the instrument.

  • Self-specify – name generic and override everything; the scene’s config is fully its own.

Why it exists, and what breaks without it. A sim scene used as a navigation fixture wants reproducible behavior. If every parameter is inherited live from a real camera’s config, then editing that camera’s star_psf_sigma (or noise, saturation, …) silently shifts the rendered image and the recovered offset of every sim scene that names it – re-blessing baselines for a change that had nothing to do with the simulator. Pinning a key via instrument_config decouples it from the camera config: the merge produces a fresh dict, so a later camera-config edit cannot reach a pinned key. Full self-specification (generic + complete overrides) makes a scene immune to all instrument-config drift. The merge is applied identically in both consumers – ObsSim.from_file and render_combined_model() – so the rendered image and the navigator’s instrument settings stay consistent. The one precedence subtlety: the top-level scene noise block (the primary noise control) still wins over instrument_config.noise for rendering, so instrument_config is the channel for the instrument parameters that have no dedicated scene field (star PSF sigma, data units, saturation / full-well DN, extfov margin, …).

Adding a new instrument

The end-to-end checklist lives at Extending the System; the obs-side bullet is:

  1. Subclass ObsSnapshotInst in src/nav/obs/obs_inst_<mission>_<inst>.py.

  2. Implement from_file() (delegate to the matching oops.hosts.<mission>.<inst> host module), star_psf(), star_min_usable_vmag(), star_max_usable_vmag(), and get_public_metadata().

  3. Register the subclass in nav.obs (src/nav/obs/__init__.py) under the per-mission / per-instrument key the corresponding DataSet subclass passes to from_file().

  4. Add the per-instrument config block at src/nav/config_files/config_4N0_inst_<mission>_<inst>.yaml so inst_config carries the instrument’s tuning knobs.

API reference

The autodocumented API surface is at nav.obs.