Backplanes
The backplanes package writes per-pixel geometry products (longitude,
latitude, incidence / emission / phase angles, ring radius, resolution, …)
for a navigated image, packaged as a multi-extension FITS file plus a sidecar
metadata JSON. Backplanes are the geometry input that downstream PDS4 bundles
ship and that operator-side analysis tools (mosaicing, photometric correction,
limb fitting) consume.
This chapter is the developer’s reference for the pipeline’s internals — the phase structure, the per-source generation algorithms, the distance-aware merge, and the FITS writer’s HDU layout. The CLI walkthrough, configurable backplane list, and FITS / sidecar shape from a user’s perspective lives at Backplane Generation.
Overview
A backplane is a sensor-shaped float array carrying one geometric quantity per
pixel. oops exposes dozens of backplane methods on its
Backplane class — every quantity the
SPICE prediction can resolve at the pixel grid (planetographic latitude,
photon-arrival timestamps, sub-solar / sub-observer angles, ring-orbit phase,
…). The backplanes pipeline picks a configurable subset of those methods,
evaluates them once per applicable source (each body in the inventory, plus
the ring system), merges the per-source results into one master array per
backplane name (so the per-pixel output is unambiguous when a body silhouette
overlaps the rings), and writes the result.
The pipeline preserves the navigation offset. nav_backplanes reads the
_metadata.json produced by nav_offset, applies the
(dv, du) offset to the
ObsSnapshot’s FOV via
oops.fov.OffsetFOV, and then evaluates every backplane. Every
output pixel is therefore the geometry that the navigation step says it is —
not the geometry that the raw SPICE prediction would have produced before the
offset correction.
Pipeline overview
Per-image, the driver runs three phases:
Build the offset-corrected snapshot. Read the per-image
_metadata.jsonfrom--nav-results-root, refuse to proceed ifstatus != 'success', build the per-instrumentObsSnapshotInstwithextfov_margin_vu=(0, 0)(backplanes are evaluated on the sensor only, not on the extended FOV used by navigation), wrap its FOV inoops.fov.OffsetFOVcarrying the navigated offset, and stash it assnapshot.Evaluate per-source backplanes.
create_body_backplanes()walks every body in the per-image inventory and, for each, builds a clipped meshgrid around the body’s bounding box (no oversampling — backplanes target sensor-resolution accuracy) and evaluates the configured body backplane methods against anoops.Backplaneover that meshgrid.create_ring_backplanes()evaluates the configured ring backplane methods against the snapshot’s full-framesnapshot.bp. Both functions return per-pixel arrays plus the per-sourcedistancearray used by phase 3.Distance-aware merge + write.
merge_sources_into_master()walks every pixel; for each pixel it picks the source with the smallest distance (closest body or ring intersection along the line of sight) and copies that source’s per-backplane values into the master arrays. The merge also fills a per-pixelBODY_ID_MAPcarrying the NAIF ID of the source that won at each pixel.write_fits()serialises the master arrays and the body-ID map to FITS, attaching theBUNITheader from the per-backplane config, and writes a companion_backplane_metadata.jsonwith per-body inventory and per-backplane min/max statistics.
Phase 1 fails the image if the navigation step did not converge; the downstream PDS4 driver also refuses to render a label for an image whose backplane FITS is missing, so a single hard failure propagates cleanly.
Entry points
nav_backplanes and nav_backplanes_cloud_tasks
(src/main/nav_backplanes.py and src/main/nav_backplanes_cloud_tasks.py)
are thin CLI wrappers around
generate_backplanes_image_files(). CLI flags,
selection options, and per-batch behaviour are documented at
Backplane Generation. Code that embeds backplane generation in a
Python pipeline calls the function directly.
Restrictions and assumptions
Snapshots only. The pipeline supports
Snapshot-derived observations only; push-broom and other observation modes are rejected at theisinstance(obs, ObsSnapshot)check. Adding a non-snapshot path requires a different per-source meshgrid construction (rate-and-state geometry) that the existing bodies / rings helpers do not implement.Sensor frame, not extfov. Backplanes are evaluated on the sensor pixel grid (
extfov_margin_vu=(0, 0)). The extended-FOV margin used by navigation does not appear in the FITS file.Offset must exist. An image whose nav
_metadata.jsonreportsoffset = Nonedefaults to(0, 0)with a warning, which means the resulting backplanes carry the raw SPICE prediction’s geometry. Operators who want to refuse those images can filter onconfidence_tierbefore running the bundle step.Per-body bounding-box evaluation. Body backplanes are evaluated on a meshgrid clipped to the body’s predicted bounding box (no oversampling). Pixels outside any body’s bounding box and outside the ring system have no backplane contribution and stay zero in the master arrays.
Per-source backplane generation
Bodies
create_body_backplanes() evaluates the
configured backplanes.bodies list against every body in the per-image
inventory. For each body:
Query the per-image inventory to get the body’s predicted bounding box (
u_min_unclipped…v_max_unclipped); clip into the sensor. Bodies with no overlap contribute nothing.Build a meshgrid of pixel centres inside the clipped bounding box, build an
oops.backplane.Backplaneover that meshgrid, and evaluate every method named inbackplanes.bodies.Mask each per-pixel array against the body silhouette (the incidence-angle backplane returns finite where the line of sight intersects the body); embed the masked array into a sensor-shaped frame so the merge step can work pixel-by-pixel without per-body coordinate math.
Compute the body’s per-pixel distance (the distance backplane on the same meshgrid) for the merge step.
Record the body’s NAIF ID and the per-backplane min/max for the sidecar JSON.
Simulated bodies (when snapshot.is_simulated) take the
_create_simulated_body_backplane path: the per-pixel array is a
synthesised constant within the simulated body’s mask
(snapshot.sim_body_mask_map[body_name] if present, otherwise the body
slot in snapshot.sim_body_index_map matched against
snapshot.sim_body_order_near_to_far). Simulation produces deterministic
fake NAIF IDs so the merge step does not crash when a simulated body has
no SPICE entry.
Rings
create_ring_backplanes() evaluates the
configured backplanes.rings list against the snapshot’s full-frame
snapshot.bp. Unlike bodies the ring backplanes do not need a
clipped meshgrid — the ring system covers a continuous range of radii
across the FOV, and oops’s ring backplanes are cheap on a
sensor-shaped grid.
The ring step also produces a per-pixel distance array (distance from
the spacecraft to the ring intersection point along the line of sight)
that the merge step compares to per-body distances to decide which source
owns each pixel.
Distance-aware merge
merge_sources_into_master() walks every pixel
exactly once. For each pixel it iterates the per-source distances and
picks the source with the smallest finite distance (closest along the
line of sight); the per-backplane values from that source are copied
into the master arrays. Pixels with no finite distance from any source
stay zero.
The function also fills a sensor-shaped BODY_ID_MAP carrying the NAIF
ID of the winning source per pixel. Bodies use their real NAIF IDs;
rings use a deterministic ring-system ID (cspyce.bodn2c('SATURN_RINGS')
or equivalent). Simulated sources use their synthesised fake IDs so the
map is well-formed even on simulated images.
The merge is symmetric in bodies and rings: a body silhouette in front of the ring plane wins because its distance is smaller; a ring inside a body silhouette (geometrically rare but possible at certain phase angles on edge-on rings) wins because its distance is smaller. The pipeline does not enforce a body-then-rings precedence order.
FITS writer
write_fits() serialises the master arrays. The
output FITS file structure:
Primary HDU — empty, conventional placeholder.
BODY_ID_MAP HDU — first ImageHDU,
int32per-pixel NAIF IDs; emitted only when at least one pixel has a non-zero ID.One HDU per backplane — name from
backplanes.bodies[i].name/backplanes.rings[i].name,BUNITheader from the per-backplaneunitsfield,float32data. Backplanes that are entirely zero on this image are omitted (a body backplane on a no-body-in-FOV image contributes no HDU).
Alongside the FITS file the writer drops a companion
<image>_backplane_metadata.json containing:
the per-image dataset / instrument / observation metadata,
the per-body inventory (NAIF ID, name, predicted bounding box),
the per-backplane min / max / mean / valid-pixel-count statistics.
The PDS4 bundle generator (PDS4 Bundle Generation) reads this sidecar when rendering the per-image data label.
Configuration
The configurable backplane list lives in
src/nav/config_files/config_900_backplanes.yaml under the backplanes
section (exposed as config.backplanes). The full YAML schema and the
shipping defaults are documented at Backplane Generation; this
section covers the developer-facing parts of the contract.
methodresolves againstoops.backplane.Backplaneby name at evaluation time. There is no compile-time check on the YAML; a typo surfaces only when the body or rings step calls the missing method on a real image. Confirm method names against theoopssource before adding an entry.The body / rings dispatch is a hard split:
backplanes.bodiesentries are evaluated against the per-body meshgrid and merged with bodydistance;backplanes.ringsentries are evaluated against the full-framesnapshot.bpand merged with ringdistance. An entry in the wrong list fails at evaluation time.The
Configloader’s deep-merge rules apply to thebackplanesblock the same way they apply elsewhere; an override file overwrites the per-source list wholesale (lists are overwritten, not appended). See Config and Static Data for the loader contract.
Per-instrument overrides are not exposed — every instrument gets the same
backplane set. An instrument-specific backplane would require a per-
instrument backplanes block in config_4N0_inst_*.yaml plus a
config-merge hook in generate_backplanes_image_files()
to splice it onto the global list.
Snapshot helpers
The backplanes pipeline relies on four helpers added to
ObsSnapshot (also consumed by the navigation
pipeline):
inventory_body_in_fov()/inventory_body_in_extfov()— consume anoopsinventory entry and return whether the predicted body bounding box overlaps the sensor / extended FOV. The body backplane step queries the inventory and uses these to decide which bodies contribute.clip_rect_fov()/clip_rect_extfov()— clamp a rectangle(u_min, u_max, v_min, v_max)into the sensor / extended FOV. The body backplane step usesclip_rect_fovon every body’s inflated inventory bounding box before building the per-body meshgrid.
These helpers are documented in Observations.
Adding a backplane
Pick the source (body or rings) and the corresponding
oopsBackplanemethod to call. Verify the method exists by reading theoopssource — there is no compile-time check on the YAML name.Append a
{name, method, units}entry to the matching list inconfig_900_backplanes.yaml. Pick anamethat scans well as a FITS HDU name (uppercase or snake_case, no spaces).Rebuild a sample image with
nav_backplanesand verify the new HDU appears in the FITS file with the expectedBUNITheader and non-trivial pixel content (usenav_backplane_viewerfor a quick visual check).If the new backplane is consumed by the PDS4 bundle, extend the per-dataset
data.lblxtemplate to declare the correspondingArray_2D_Imageelement and updatepds4_template_variables()to surface the relevant per-HDU stats (min, max, units) into the template’s variable dict. See PDS4 Bundle Generation.
Testing
A smoke test under experiments/backplanes/ drives the end-to-end
pipeline against a simulated-image JSON, verifies the FITS file
structure, and asserts that simulated per-body backplanes respect each
body’s mask (so a fake backplane does not paint a rectangular block
beyond the simulated body’s edge).
Per-component unit tests under tests/backplanes/ (where they exist)
cover the merge-step distance comparison, the per-body bounding-box
clipping, and the writer’s BODY_ID_MAP emission rule.
API reference
The backplanes package has no autogenerated entry under
API Reference; the public surface is the five functions listed
below:
generate_backplanes_image_files()— per-image driver.create_body_backplanes()— body source.create_ring_backplanes()— ring source.merge_sources_into_master()— distance-aware merge.write_fits()— FITS + sidecar writer.