spindoctor.support

Shared utilities for navigation code.

This package groups small, reusable helpers. Import from submodules directly, for example from spindoctor.support import image or from spindoctor.support.image import shift_array.

Modules:

types

NumPy typing aliases (e.g. NDArrayFloatType), PathLike, and protocols such as MutableStar.

image

Two-dimensional array helpers: shifting, padding, cropping, normalization, and FFT-related image operations.

correlate

Fourier-domain and template-matching utilities (e.g. normalized cross-correlation) built on image and misc.

misc

Miscellaneous helpers, including sky-coordinate formatting and oops-backed utilities.

time

Wall-clock helpers: ISO strings, timezone-aware datetimes, and Julian conversions.

file

YAML/JSON serialization helpers and clean_obj for stripping NumPy scalars from nested structures.

constants

Common mathematical constants (e.g. PI, HALFPI).

exceptions

NavContractError — typed exception for internal contract violations.

nav_base

NavBase, a small base class wiring Config and PdsLogger for nav objects.

attrdict

AttrDict, a dict subclass that supports attribute-style key access.

flux

Legacy flux and filter-convolution experiments; most of the implementation is commented out but kept for reference.

filters

NavFilterSpec / NavFilterKind and the dispatcher apply_filter used across feature extraction and matching techniques.

filter_combo

canonicalize for normalizing multi-filter combos into a stable key.

status_reason

NavStatusReason enum carried on every NavResult.

noise_estimate

estimate_image_noise_sigma — robust per-image noise estimator.

image_quality

saturation_mask and cosmic_ray_mask — global image-quality masks consumed by extractors.

distance_transform

apply_translation and sample_dt_bilinear — chamfer-matching helpers built on top of an externally-computed distance transform.

class AttrDict(*args: Any, **kwargs: Any)[source]

Bases: dict[str, Any]

Implements a dictionary that allows attribute-style access to its key-value pairs.

A dictionary subclass that exposes its keys as attributes, allowing dict items to be accessed using attribute notation (dict.key) in addition to the normal dictionary lookup (dict[key]).

Parameters:
  • *args – Variable length argument list passed to dict constructor.

  • **kwargs – Arbitrary keyword arguments passed to dict constructor.

Constants module for navigation calculations.

This module defines mathematical constants used throughout the navigation system.

evaluate_candidate(*, image_pad: NDArray[floating[Any]], model_pad: NDArray[floating[Any]], mask_pad: NDArray[bool], corr: NDArray[floating[Any]], rc: tuple[int, int], upsample_factor: int, model_shape: tuple[int, int], image_shape: tuple[int, int], logger: PdsLogger, prior_shift: tuple[float, float] | None = None, prior_weight: float = 0.0, metric: str = 'psr', refine_image_pad: NDArray[floating[Any]] | None = None, refine_model_pad: NDArray[floating[Any]] | None = None, refine_lowpass_sigma_px: float = 0.0) dict[str, Any][source]

Evaluate a candidate for the navigation.

Parameters:
  • image_pad – The padded image.

  • model_pad – The padded model.

  • mask_pad – The padded mask.

  • corr – The correlation matrix.

  • rc – The row and column of the candidate.

  • upsample_factor – The upsample factor.

  • model_shape – The shape of the model.

  • image_shape – The shape of the image.

  • prior_shift – The prior shift.

  • prior_weight – The prior weight.

  • metric – The metric to use for the navigation.

  • refine_image_pad – Optional padded image used only for the sub-pixel cross-power-spectrum refinement, in place of image_pad. When the coarse peak is found on gradient-magnitude surfaces (use_gradient), pass the raw-intensity padded image here: the Sobel magnitude rectifies the signal, so its cross-power peak is non-smooth at the apex and the upsampled-DFT sub-pixel estimate is biased (largest near whole-pixel offsets), whereas raw intensity reaches the upsample_factor resolution. Defaults to image_pad.

  • refine_model_pad – Optional padded model for the same refinement; defaults to model_pad.

Returns:

A dictionary containing the navigation result.

Raises:

ValueError – If the model is smaller than the image in either dimension (the model must be at least image-sized; pad it with pad_top_left first). This is validated here so the failure is a clear contract error rather than an opaque crop_center error two calls deep.

fourier_shift(img: NDArray[floating[Any]], dy: float, dx: float) NDArray[floating[Any]][source]

Subpixel shift via Fourier shift theorem (positive = down/right).

gradient_magnitude(arr: NDArray[floating[Any]]) NDArray[floating[Any]][source]

Sobel gradient magnitude.

Emphasizes edges over flat interior regions, which is what you want when the raw-intensity NCC has a broad plateau — e.g., a lit body disc that overflows the FOV, where interior brightness is near-uniform and the only unique-alignment signal lies at the limb.

Parameters:

arr – 2-D input array.

Returns:

Same-shape gradient-magnitude array, as float64.

int_to_signed(idx: int, size: int) int[source]

Map [0 .. size-1] argmax index to a signed displacement coordinate.

Parameters:
  • idx – Peak index from correlation search in [0, size).

  • size – FFT / correlation length (modulus for wrapping).

Returns:

int signed offset equivalent to idx when idx < size // 2, else idx - size (negative branch for peaks in the second half).

masked_ncc(image: NDArray[floating[Any]], model: NDArray[floating[Any]], mask: NDArray[bool], data_mask: NDArray[bool] | None = None) tuple[NDArray[floating[Any]], NDArray[floating[Any]]][source]

Masked normalized cross-correlation surface between image and model.

Computes shift-wise NCC (Pearson r) using FFT-based sums. Also returns the unnormalized NCC numerator (mean-subtracted covariance) as a diagnostic.

The NCC surface contains values in [-1, 1] at every shift where the mask overlaps non-constant image content, and is the primary peak-selection surface: it is invariant to the image variance under the shifted mask and therefore does not suffer from the “scale bias” that causes the numerator to prefer shifts where the fixed template mask straddles bright/dark boundaries in the image (e.g., the limb of a Lambert-shaded body disc). The numerator surface scales with sqrt(image-variance-under-mask) times the NCC, which is useful as a sanity check but must not be used for peak finding when the template is dense.

Parameters:
  • image – Padded image array.

  • model – Padded model array (same shape as image).

  • mask – Padded boolean mask (same shape as image). True where model pixels are valid.

  • data_mask – Optional padded boolean mask indicating where image contains real sensor data (True) versus zero-padded margin (False). When provided, the NCC is computed in its bi-directional form so that pixel pairs where the shifted model mask straddles zero-padded image pixels do not bias the normalization. This removes the edge-ridge artifact that otherwise appears at |dV| = extfov_margin_v when the body model extends into the extfov margin. When None, the standard (single-mask) NCC formula is used.

Returns:

Tuple of (ncc, numerator) arrays, each with the same shape as the inputs. ncc is the Pearson-r surface used for peak localization; numerator is the mean-subtracted cross-covariance (unnormalized) returned as a diagnostic. When data_mask is provided, shifts with degenerate overlap or variance are set to -inf in the NCC surface.

matched_filter_covariance(model_aligned: NDArray[floating[Any]], sigma_n: float, *, correlation_area: float = 1.0) NDArray[floating[Any]][source]

Peak-curvature (matched-filter) covariance of a 2-D translation fit.

For a template M aligned to an image I = M(x - s) + n the shift s maximum-likelihood estimate has Fisher information F_ab = (1 / sigma_n**2) * sum_x (dM/dx_a)(dM/dx_b) and covariance F**-1 = sigma_n**2 * inv(sum grad M grad M^T).

Two properties are essential for the reported sigma to track the true error and are the reason this replaces the previous derivation:

  • Unit consistency. sigma_n and the gradients of M must be measured on the same intensity scale, or the covariance scales with the (arbitrary) template amplitude. The caller passes a residual noise sigma_n computed on the zero-mean, unit-std normalized image and model; this function must therefore receive the same normalized model so grad M shares that scale. Passing the raw template here (whose amplitude can be thousands of DN) shrinks the covariance by the template variance and is the miscalibration this derivation corrects.

  • Effective sample count. correlation_area inflates the white-noise bound by the residual’s spatial correlation area so correlated model error is not counted as thousands of independent constraints (see _residual_correlation_area()).

Parameters:
  • model_aligned – The aligned template on the same normalized scale as the residual from which sigma_n was measured.

  • sigma_n – Residual noise standard deviation on that normalized scale.

  • correlation_area – Residual correlation area in pixels (>= 1); multiplies the covariance. Default 1.0 reproduces the white-noise Cramer-Rao bound.

Returns:

The 2x2 translation covariance in pixels squared. A degenerate (rank-deficient) gradient structure returns a large isotropic covariance so the result is de-weighted rather than trusted.

navigate_single_scale_kpeaks(*, image: NDArray[floating[Any]], model: NDArray[floating[Any]], mask: NDArray[bool], logger: PdsLogger | None, max_peaks: int = 5, upsample_factor: int = 16, metric: str = 'psr', prior_shift: tuple[float, float] | None = None, prior_weight: float = 0.0, nms_radius: int = 5, max_offset_vu: tuple[int, int] | None = None, data_mask: NDArray[bool] | None = None, use_gradient: bool = False, refine_lowpass_sigma_px: float = 0.0) dict[str, Any][source]

One-scale masked NCC + top-K candidate evaluation.

Parameters:
  • image – The image to navigate.

  • model – The model to navigate.

  • mask – The mask to use for the navigation.

  • max_peaks – The number of peaks to use for the navigation.

  • upsample_factor – The upsample factor to use for the navigation.

  • metric – The metric to use for the navigation.

  • prior_shift – The prior shift to use for the navigation.

  • prior_weight – The prior weight to use for the navigation.

  • nms_radius – The radius to use for the non-maximum suppression.

  • logger – The logger to use for the navigation.

  • max_offset_vu – If given, only correlation peaks whose signed (V, U) offset satisfies |dV| <= max_offset_vu[0] and |dU| <= max_offset_vu[1] are considered candidates. Typically set to the extended-FOV margins so that offsets outside the physically-plausible range are never evaluated.

  • data_mask – Optional boolean mask (same shape as image) that is True where the image contains real sensor data and False inside the zero-padded extended-FOV margin. When provided, the NCC is computed in its bi-directional form so that the model extending into the padded margin does not bias the peak toward |dV| = margin_v.

  • use_gradient – When True, replace image and model with their Sobel gradient magnitudes before the NCC. Use this when the raw intensity surface has a broad plateau — e.g., a body that fills or overflows the FOV, where the only unique-alignment signal is at the limb.

Returns:

A dictionary containing the navigation result.

navigate_with_pyramid_kpeaks(image: NDArray[floating[Any]], model: NDArray[floating[Any]], mask: NDArray[bool], pyramid_levels: int = 3, max_peaks: int = 5, upsample_factor: int = 128, metric: str = 'psr', quality_thresh: float = 6.0, consistency_tol: float = 2.0, nms_radius: int = 5, prior_weight_final: float = 0.25, max_offset_vu: tuple[int, int] | None = None, data_mask: NDArray[bool] | None = None, use_gradient: bool | Literal['auto'] = False, refine_lowpass_sigma_px: float = 0.0, localization_uncertainty_scale: float = 0.0, logger: PdsLogger | None = None) dict[str, Any][source]

TODO Clean this up Build class-aware effective model + mask, run coarse->fine, then evaluate K peaks at final scale. Returns dict with shift, covariance, sigma_xy, quality, consistency, spurious flag.

Parameters:
  • image – The source image to navigate, unpadded.

  • model – The model to navigate against, padded as necessary to include more data around the edges. It does not need to be the same size as the image.

  • mask – The mask indicating which pixels in the model are valid. Same size as the model.

  • pyramid_levels – The number of pyramid levels to use. Each pyramid level divides the image and model by an additional factor of 2 (pyramid_levels=3 means to start with 1/4, then 1/2, then 1/1 downsampling).

  • max_peaks – The number of peaks to look for in the correlation at each pyramid level.

  • upsample_factor – The upsample factor to use for increased FFT resolution around a peak.

  • metric – The metric to use for the navigation. Can be one of ‘psr’, ‘pmr’, or ‘per’.

  • quality_thresh – The quality threshold to use for the navigation.

  • consistency_tol – The consistency tolerance to use for the navigation.

  • nms_radius – The radius to use for the non-maximum suppression.

  • prior_weight_final – The prior weight to use for the final navigation.

  • max_offset_vu – Maximum permitted signed offset as (max_dV, max_dU) in full-resolution pixels. When given, correlation peaks outside this range are excluded at every pyramid level (the limit is scaled by the downsample factor at each level). Pass obs.extfov_margin_vu to restrict the search to offsets that are physically reachable given the extended FOV padding.

  • data_mask – Optional boolean mask (same shape as image) that is True where the image contains real sensor data and False inside the zero-padded extended-FOV margin. When provided, the NCC is computed in its bi-directional form at every pyramid level so that a body model extending into the extfov margin does not bias the peak toward |dV| = margin_v. Pass obs.extfov_data_sensor_mask().

  • use_gradient

    Controls gradient-magnitude preprocessing of the image and model before the NCC.

    • False (default): raw-intensity NCC at every pyramid level.

    • True: gradient-magnitude NCC at every pyramid level. Use this when the raw surface has a broad plateau — e.g., a body that fills or overflows the FOV, where only the limb carries unique-alignment signal.

    • 'auto': run both modes, pick the more confident result. A non-spurious result is preferred over a spurious one; within the same spurious bucket the higher-quality result wins.

  • localization_uncertainty_scale – Scales the inter-pyramid-level peak migration (consistency, in px) into an added translation uncertainty. The peak-curvature covariance measures only the statistical (photon / residual) precision at the winning peak; a peak that walks between pyramid levels is empirically less well localized than a peak that does not, so (scale * consistency)**2 is added in quadrature to the translation covariance diagonal. 0.0 (default) disables it and leaves the bare peak-curvature covariance.

  • logger – The logger to use for the navigation.

Returns:

  • offset: The offset.

  • cov: The covariance matrix.

  • sigma_xy: The sigma_xy.

  • quality: The quality of the navigation.

  • metric: The metric used for the navigation.

  • consistency: The consistency of the navigation.

  • spurious: True if the navigation is spurious, False otherwise.

Return type:

A dictionary containing the navigation result

Notes

The metrics are:

  • PSR (Peak-to-Sidelobe Ratio): Measures peak distinctness as (peak - mean_sidelobe) / std_sidelobe, where the sidelobe region excludes the peak neighborhood.

  • PMR (Peak-to-Mean Ratio): Ratio of the global maximum correlation value to the mean of all correlation values; indicates how dominant the main peak is over the average background.

  • PER (Peak-to-Energy Ratio): Ratio of the squared peak value to the total correlation energy (sum of squares); reflects how much of the total response energy is concentrated in the main peak.

nms_topk(corr: NDArray[floating[Any]], k: int = 5, radius: int = 5, max_offset_vu: tuple[int, int] | None = None) list[tuple[int, int, float]][source]

Non-maximum suppression to get top-k peaks.

Parameters:
  • corr – 2-D correlation surface (V x U).

  • k – Maximum number of peaks to return.

  • radius – Suppression radius around each selected peak in pixels.

  • max_offset_vu – If given, only positions whose signed offset satisfies |dV| < max_offset_vu[0] and |dU| < max_offset_vu[1] are eligible (strict inequality: peaks at the exact window boundary are excluded because they signal a clipped search and cannot be trusted; the pyramid driver additionally marks any final result within 1 pixel of the boundary as spurious). Signed offsets are derived from the FFT-convention wrap-around used by int_to_signed().

Returns:

List of (row, col, value) tuples for up to k peaks.

peak_to_runner_up_ratio(top_k_peaks: list[tuple[float, float, float]]) float[source]

Return the ratio of the winning peak’s quality to the runner-up’s.

top_k_peaks is [(quality, dv, du), ...] sorted by quality descending (the convention navigate_with_pyramid_kpeaks() uses). Returns 1.0 when only one peak survives non-maximum suppression – what an unambiguous correlation looks like, so a value at or above 1.0 is the “good” tail – and 0.0 when no peaks are present. When the runner-up quality is non-positive (rare; happens with the prior penalty) the result is the unambiguous-winner cap _MAX_PEAK_RATIO rather than winner / 1e-9 (which scaled with the winner’s magnitude and could reach ~1e9). The ordinary ratio is likewise clamped to _MAX_PEAK_RATIO.

Parameters:

top_k_peaks – Peaks sorted by quality descending.

Returns:

Capped peak-to-runner-up quality ratio.

per_metric(corr: NDArray[floating[Any]], peak_val: float) float[source]
pmr_metric(corr: NDArray[floating[Any]], peak_val: float) float[source]
psr_metric(corr: NDArray[floating[Any]], rc: tuple[int, int], guard: int = 5) float[source]
upsampled_dft(X: NDArray[complexfloating[Any, Any]], up_factor: int, region_sz: tuple[int, int], offsets: tuple[int, int]) NDArray[complexfloating[Any, Any]][source]

Localized upsampled DFT.

From Guizar-Sicairos, 2008. “Efficient subpixel image registration via cross-correlation.” Optics Leters, 33(2):156-158

Chamfer-matching helpers built on top of the distance transform.

DT-based techniques (BodyLimbNav, BodyTerminatorNav, RingEdgeNav) all rely on the same primitive: given an image-side distance transform of an edge map and a model polyline, evaluate the cost of a candidate offset by sampling the DT at the shifted polyline vertices.

This module owns the polyline-shifting and DT-sampling routines so the three techniques don’t reimplement them. Bilinear DT interpolation is used for sub-pixel precision; the orchestrator’s NavContext carries the per-image DT array so callers reach in once and the gradient is shared.

apply_translation(vertices_vu: NDArray[floating[Any]], dv: float, du: float) NDArray[floating[Any]][source]

Return vertices_vu shifted by (dv, du).

Parameters:
  • vertices_vu(N, 2) array of (v, u) vertex positions.

  • dv – Shift along the v axis (rows).

  • du – Shift along the u axis (columns).

Returns:

(N, 2) shifted vertex positions; original is not modified.

Raises:

ValueError – if vertices_vu is not (N, 2).

sample_dt_bilinear(dt: NDArray[floating[Any]], vertices_vu: NDArray[floating[Any]]) NDArray[floating[Any]][source]

Bilinear-sample a distance transform at sub-pixel vertex positions.

Vertices outside dt are clamped to the boundary value at the closest in-bounds pixel. The result is the DT cost contribution per vertex; the sum (or the weighted sum, with M-estimator weights) is what an LM refinement minimizes.

Parameters:
  • dt – 2-D distance-transform array (output of apply_filter with DISTANCE_TRANSFORM kind, or an externally-built DT).

  • vertices_vu(N, 2) array of (v, u) sub-pixel vertex positions.

Returns:

(N,) array of bilinear-interpolated DT values at each vertex.

Raises:

ValueError – if shape requirements are violated.

Typed exceptions shared across the navigation core.

Contract violations must not be expressed as bare assert statements: asserts are stripped under python -O, and inside the orchestrator’s broad plugin sandboxes an AssertionError would be swallowed as an ordinary technique failure. Raising NavContractError instead keeps the check active in optimized runs and lets the orchestrator treat the violation distinctly (error-level log plus a failed NavResult with NavStatusReason.CONTRACT_VIOLATION).

exception NavContractError[source]

Bases: Exception

An internal navigation invariant (contract) was violated.

Raised when an upstream component hands core code a value outside its documented bounds (for example a 3-DoF technique result whose rotation exceeds the ensemble’s small-angle bound). A NavContractError always indicates a programming error, never bad image data, so it is never swallowed by the orchestrator’s plugin sandboxes: the sandboxes log it at error level and re-raise, and NavOrchestrator.navigate converts it into a failed NavResult with NavStatusReason.CONTRACT_VIOLATION.

clean_obj(obj: Any) Any[source]

Recursively converts NumPy types in any object to Python native types.

Not a pure function: a dict argument (and any nested dict) is converted in place – each NumPy scalar is replaced by its Python native equivalent in the caller’s own dict – and the same object is returned. A list/tuple argument yields a new list (so the caller’s sequence object is not mutated), but dict values nested inside it are still mutated in place. Callers that must keep the original untouched should pass a deep copy.

Parameters:

obj – The object to clean, can be a dict, list, tuple or scalar value.

Returns:

The object with all NumPy types converted to Python native types (the same dict object when a dict is passed; see the mutation note).

dump_yaml(data: Any, stream: Any = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>) None[source]

Dumps data as YAML output after converting NumPy types to Python types.

Parameters:

data – The data to dump as YAML.

json_as_string(data: Any) str[source]

Dumps data as a JSON string after converting NumPy types to Python types.

Parameters:

data – The data to dump as JSON.

Canonical form of a multi-filter optical combo string.

Most missions in this pipeline carry one or two filter names per exposure (e.g. Cassini ISS uses two filter wheels per camera; Voyager and Galileo use one). Multiple call sites need a canonical string for the same combo:

  • mag_offset_table lookup keys.

  • Test image-library sidecar filter_combo field.

  • Per-image log lines and metadata fields.

This module supplies the single canonicalize rule (alphabetic-sort joined by '+') so the spelling never drifts between consumers.

canonicalize(filters: Sequence[str | None]) str[source]

Return a canonical string representation of a filter combo.

Drops None entries, sorts the remaining filter names alphabetically, joins them with '+'. Duplicate names are preserved (so ['CL', 'CL'] becomes 'CL+CL'). An empty sequence (or a sequence containing only None entries) returns 'NONE'.

Parameters:

filters – Iterable of filter name strings; None entries are dropped.

Returns:

Canonical '+'-joined sorted filter combo string, or 'NONE' if no non-None filters are given.

Examples

canonicalize([]) -> 'NONE' canonicalize(['CL1']) -> 'CL1' canonicalize(['CL2', 'CL1']) -> 'CL1+CL2' canonicalize(['CL', 'CL']) -> 'CL+CL' canonicalize(['F1', None, 'F2']) -> 'F1+F2'

Filter abstraction used by feature extractors and matching techniques.

Each NavFeature carries a preferred_filter NavFilterSpec; the consuming technique applies the spec to both the image patch and the model template before computing its matching metric. A small number of kinds covers every feature type used by the v1 pipeline.

The apply_filter entry point dispatches on NavFilterKind and runs the configured operation, with two universal short-circuits:

  • NavFilterKind.NONE returns the input unchanged.

  • A spec whose largest principal sigma is below null_filter_threshold_sigma (per-config) is treated as NONE.

Higher-level technique code never indexes by kind itself; it just calls apply_filter(arr, spec).

Thread safety: all functions in this module are pure / stateless; safe for concurrent use on independent inputs.

class NavFilterKind(*values)[source]

Bases: Enum

The kind of operation a NavFilterSpec describes.

  • NONE: identity filter; apply_filter returns the input unchanged.

  • ISOTROPIC_GAUSSIAN: symmetric Gaussian blur with a single sigma_xy.

  • ANISOTROPIC_GAUSSIAN: possibly axis-aligned Gaussian blur with a full 2x2 covariance. align_axis may rotate the blur into a non-axis-aligned principal frame.

  • BANDPASS_DOG: difference-of-Gaussians bandpass; subtract a heavy-blur from a light-blur of the input to suppress low-frequency content while preserving sharper detail.

  • DISTANCE_TRANSFORM: signed distance transform of a thresholded edge map; only meaningful as a precomputed image-side quantity, not a generic operator. apply_filter raises if asked to apply it to a non-binary array.

  • GRADIENT_OF_GAUSSIAN: gradient magnitude of a Gaussian-smoothed input; isotropic blur followed by Sobel.

  • MORPH_DILATE: morphological dilation by a structuring element of half-width derived from sigma_xy. Used when building search margins for edge-based matching.

ANISOTROPIC_GAUSSIAN = 'ANISOTROPIC_GAUSSIAN'
BANDPASS_DOG = 'BANDPASS_DOG'
DISTANCE_TRANSFORM = 'DISTANCE_TRANSFORM'
GRADIENT_OF_GAUSSIAN = 'GRADIENT_OF_GAUSSIAN'
ISOTROPIC_GAUSSIAN = 'ISOTROPIC_GAUSSIAN'
MORPH_DILATE = 'MORPH_DILATE'
NONE = 'NONE'
class NavFilterSpec(kind: NavFilterKind, sigma_xy: tuple[float, float] = (0.0, 0.0), covariance_px2: NDArray[floating[Any]] | None = None, bandpass_cutoffs_px: tuple[float, float] = (0.0, 0.0), dt_half_width_px: float = 0.0, align_axis: tuple[float, float] | None = None, null_filter_threshold_sigma: float = 0.4)[source]

Bases: object

Description of a filter to be applied uniformly to image and template.

All fields except kind are optional; only the ones the chosen kind consumes need to be set. Tests assert that mismatched kind/parameter combinations raise on application, not at construction (so techniques can carry under-populated specs through identity short-circuits without extra ceremony).

Parameters:
  • kind – Which kind of filter operation this spec describes.

  • sigma_xy – Per-axis Gaussian sigma in pixels, used by ISOTROPIC_GAUSSIAN, GRADIENT_OF_GAUSSIAN, and MORPH_DILATE.

  • covariance_px2 – 2x2 covariance matrix used by ANISOTROPIC_GAUSSIAN. None for other kinds.

  • bandpass_cutoffs_px(lo_sigma, hi_sigma) in pixels for BANDPASS_DOG. Only used by that kind.

  • dt_half_width_px – Half-width truncation of distance values, in pixels. Used only by DISTANCE_TRANSFORM.

  • align_axis – Optional (v, u) direction to align the principal axis of an anisotropic filter. None means axis-aligned (identity rotation).

align_axis: tuple[float, float] | None = None
bandpass_cutoffs_px: tuple[float, float] = (0.0, 0.0)
covariance_px2: NDArray[floating[Any]] | None = None
dt_half_width_px: float = 0.0
kind: NavFilterKind
null_filter_threshold_sigma: float = 0.4
sigma_xy: tuple[float, float] = (0.0, 0.0)
apply_filter(arr: NDArray[floating[Any]], spec: NavFilterSpec) NDArray[floating[Any]][source]

Apply spec to arr and return the filtered array.

Two universal short-circuits run before kind dispatch:

  1. spec.kind == NavFilterKind.NONE returns arr unchanged.

  2. If the largest principal sigma of spec is below spec.null_filter_threshold_sigma, the spec is too small to make a meaningful difference; the array is returned unchanged.

Otherwise the operation indicated by spec.kind is run.

Parameters:
  • arr – 2-D float input array.

  • spec – NavFilterSpec describing the operation.

Returns:

Filtered 2-D float array; same shape as the input.

Raises:
  • ValueError – if spec is missing parameters required by its kind (e.g. ANISOTROPIC_GAUSSIAN without a covariance matrix).

  • TypeError – if arr is not 2-D.

clean_sclass(sclass: str | None) str[source]

Return a clean stellar classification such as A0 or M8.

apply_linear_gamma_stretch(data: NDArray[floating[Any]], *, black: float, white: float, gamma: float) NDArray[floating[Any]][source]

Apply black/white/gamma stretch and return a float array in [0, 1].

Uses the convention ((clip(data, black, white) - black) / (white - black)) ** gamma. A gamma of 1.0 is linear; values below 1.0 brighten the mid-tones (common for display) and values above 1.0 darken them.

Parameters:
  • data – Input float array (any shape).

  • black – Black-point; input values at or below this map to 0.

  • white – White-point; input values at or above this map to 1. If white <= black (including accidental UI equality), white is raised silently to the next representable float above black so the linear scale denominator is never zero.

  • gamma – Exponent applied after linear normalisation; must be finite and strictly greater than zero.

Returns:

Float array of the same shape as data with values in [0, 1].

Raises:
  • TypeError – If black, white, or gamma is not an int or float, or any of them is a bool.

  • ValueError – If any of black, white, or gamma is not finite; or if gamma <= 0.

array_unzoom(array: NDArray[NPType], factor: int | list[int] | tuple[int, ...], method: str = 'mean') NDArray[NPType][source]

Zoom down by an integer factor. Returned arrays are floating-point.

array_zoom(a: NDArray[NPType], factor: list[int] | tuple[int, ...]) NDArray[NPType][source]

Zooms an array by the specified factor using array indexing.

Parameters:
  • a – The input array to zoom.

  • factor – The zoom factor for each dimension of the array.

Returns:

The zoomed array with dimensions multiplied by the corresponding factors.

crop_center(img: NDArray[NPType], out_shape: tuple[int, int]) NDArray[NPType][source]

Center crop to out_shape (h,w).

Parameters:
  • img – A 2-D array to crop.

  • out_shape – The target shape (height, width).

Returns:

The center-cropped array.

Raises:

ValueError – If img is not 2-dimensional or out_shape is larger than img.shape.

draw_circle(img: NDArray[NPType], color: int | float | list[int | float] | tuple[int | float, ...], x0: int, y0: int, r: int, thickness: int = 1) None[source]

Draw a circle using Bresenham’s algorithm with the given thickness.

Parameters:
  • img – The 2-D (or higher) array to draw on.

  • x0 – The middle of the circle.

  • y0 – The middle of the circle.

  • r – The radius of the circle.

  • color – The scalar (or higher) color to draw.

  • thickness – The thickness (total width) of the circle.

draw_line(img: NDArray[NPType], color: int | float | list[int | float] | tuple[int | float, ...], x0: int | float, y0: int | float, x1: int | float, y1: int | float, thickness: float = 1.0) None[source]

Draw a line using Bresenham’s algorithm with the given thickness.

The line is drawn by drawing each point as a line perpendicular to the main line.

Parameters:
  • img – The 2-D (or higher) array to draw on.

  • x0 – The starting point.

  • y0 – The starting point.

  • x1 – The ending point.

  • y1 – The ending point.

  • color – The scalar (or higher) color to draw.

  • thickness – The thickness (total width) of the line.

draw_line_arrow(img: NDArray[NPType], color: int | float | list[int | float] | tuple[int | float, ...], x0: int | float, y0: int | float, x1: int | float, y1: int | float, thickness: float = 1.0, arrow_head_length: int = 10, arrow_head_angle: int = 45) None[source]

Draws a line with an arrow head at the end point.

Parameters:
  • img – The 2-D (or higher) array to draw on.

  • color – The scalar (or higher) color to draw.

  • x0 – The x-coordinate of the starting point.

  • y0 – The y-coordinate of the starting point.

  • x1 – The x-coordinate of the ending point.

  • y1 – The y-coordinate of the ending point.

  • thickness – The thickness of the line.

  • arrow_head_length – The length of the arrow head lines.

  • arrow_head_angle – The angle in degrees between the arrow head and the main line.

draw_rect(img: NDArray[NPType], color: bool | int | float | list[int | float] | tuple[int | float, ...], xctr: int, yctr: int, xhalfwidth: int, yhalfwidth: int, thickness: int = 1, dot_spacing: int = 1) None[source]

Draw a rectangle with the given line thickness.

Parameters:
  • img – The 2-D (or higher) array to draw on.

  • color – The scalar (or higher) color to draw.

  • xctr – The horizontal (column) center of the rectangle.

  • yctr – The vertical (row) center of the rectangle.

  • xhalfwidth – The horizontal half-width, on each side of the center.

  • yhalfwidth – The vertical half-width, on each side of the center.

  • thickness – The thickness (total width) of the line.

  • dot_spacing – The spacing between dots in the rectangle. 1 means dots are adjacent (solid lines).

All slice bounds are clipped to the array extent so a center near or beyond the image edge cannot wrap a negative index around to the far side and paint a spurious rectangle; an entirely off-image rectangle draws nothing.

filter_downsample(arr: NDArray[floating[Any]], amt_y: int, amt_x: int) NDArray[floating[Any]][source]

Downsamples an array by averaging blocks of pixels.

Parameters:
  • arr – The input array to downsample.

  • amt_y – The vertical downsampling factor.

  • amt_x – The horizontal downsampling factor.

Returns:

The downsampled array.

Raises:

AssertionError – If the array dimensions are not divisible by the downsampling factors.

filter_local_maximum(data: NDArray[NPType], maximum_boxsize: int = 3, median_boxsize: int = 11, maximum_blur: int = 0, maximum_tolerance: float = 1.0, minimum_boxsize: int = 0, gaussian_blur: float = 0.0) NDArray[NPType][source]

Filter an array to find local maxima.

Process:
  1. Create a mask consisting of the pixels that are local maxima within maximum_boxsize

  2. Find the minimum value for the area around each array pixel using minimum_boxsize

  3. Remove maxima that are not at least maximum_tolerange times the local minimum

  4. Blur the maximum pixels mask to make each a square area of maximum_blur X maximum_blur

  5. Compute the median-subtracted value for each array pixel using median_boxsize

  6. Copy the median-subtracted array pixels to a new zero-filled array where the maximum mask is true

  7. Gaussian blur this final result

Parameters:
  • data – The array

  • maximum_boxsize – The box size to use when finding the maximum value for the area around each pixel.

  • median_boxsize – The box size to use when finding the median value for the area around each pixel.

  • maximum_blur – The amount to blur the maximum filter. If a pixel is marked as a maximum, then the pixels in a blur X blur square will also be marked as a maximum.

  • maximum_tolerance – The factor above the local minimum that a maximum pixel has to be in order to be included in the final result.

  • minimum_boxsize – The box size to use when finding the minimum value for the area around each pixel.

  • gaussian_blur – The amount to blur the final result.

Returns:

The filtered array.

filter_sub_median(data: NDArray[floating[Any]], median_boxsize: int = 11, gaussian_blur: float = 0.0, footprint: str = 'square') NDArray[floating[Any]][source]

Compute the median-subtracted value for each pixel.

Parameters:
  • data – The array

  • median_boxsize – The box size to use when finding the median value for the area around each pixel.

  • gaussian_blur – The amount to blur the median value before subtracting it from the array.

  • footprint – The shape of footprint to use (‘square’, ‘circle’).

Returns:

The median-subtracted array.

gaussian_blur_cov(img: NDArray[floating[Any]], sigma: NDArray[floating[Any]]) NDArray[floating[Any]][source]

Blur by anisotropic Gaussian with covariance matrix in frequency domain.

Parameters:
  • img – A 2-D array to blur.

  • sigma – A 2x2 covariance matrix [[Syy, Syx], [Sxy, Sxx]].

Returns:

The blurred image.

Raises:

ValueError – If img is not 2-dimensional or sigma is not 2x2.

gradient_magnitude(img: NDArray[floating[Any]]) NDArray[floating[Any]][source]

Compute isotropic gradient magnitude.

Parameters:

img – The input array.

Returns:

The gradient magnitude at each pixel, computed as sqrt(gx^2 + gy^2).

next_power_of_2(n: int) int[source]

Computes the smallest power of 2 that is greater than or equal to n.

Parameters:

n – A non-negative integer.

Returns:

The smallest power of 2 that is >= n. 0 returns 1 (the smallest power of 2 that is >= 0).

Raises:

ValueError – If n is negative (a power of 2 >= a negative number is ill-defined, and bin() of a negative would be misparsed).

normalize_array(a: NDArray[floating[Any]], eps: float = 1e-12) NDArray[floating[Any]][source]

Zero-mean, unit-std normalization (safe if nearly constant).

Parameters:
  • a – The input array to normalize.

  • eps – Minimum standard deviation threshold. If std < eps, returns zeros.

Returns:

Normalized array with mean 0 and std 1, or zeros if nearly constant.

pad_array(array: NDArray[NPType], margin: list[int] | tuple[int, ...], fill: Any = 0) NDArray[NPType][source]

Pad an array by the given margin at each edge.

Parameters:
  • array – The N-dimensional array to pad.

  • margin – A list or tuple of length N of margin amounts in the same order as the array indices. Each axis is padded to its original size plus 2*N and the original values are centered on the new axis.

  • fill – The value used to fill the newly created array elements.

Returns:

an all-zero margin returns the same array object (no copy); a non-zero margin returns a fresh np.pad array.

Return type:

The array padded by the given amount. Aliasing note

Raises:

ValueError – If the array shape and margin list have different lengths.

pad_array_to_power_of_2(data: NDArray[NPType]) tuple[NDArray[NPType], tuple[int, ...]][source]

Zero-pad a 2-D array on all sides to be a power of 2 in each dimension.

If the original array has a side that is not a power of 2, it must be even.

Returns: A tuple containing the padded array and the amount of padding added.

pad_top_left(array: NDArray[NPType], v_size: int, u_size: int) NDArray[NPType][source]

Place array at (0,0) inside zeros(v_size, u_size).

Parameters:
  • array – A 2-D array to place.

  • v_size – The height of the output array.

  • u_size – The width of the output array.

Returns:

A (v_size, u_size) array with the input array at the top-left corner. If the input array is larger than the target size, it will be truncated.

require_finite_int_or_float(name: str, value: object) None[source]

Validate stretch control numeric parameters at the public API boundary.

Used by stretch helpers and the UI control-builders to reject bool, non-numeric, and non-finite inputs with a clear error message tagged by the parameter name.

Parameters:
  • name – Parameter name to include in the error message (e.g. 'black').

  • value – Candidate value to validate.

Raises:
  • TypeError – When value is a bool or not an int/float.

  • ValueError – When value is not finite.

shift_array(array: NDArray[NPType], offset: int | list[int] | tuple[int, ...], fill: Any = 0) NDArray[NPType][source]

Shift an array by an offset, filling with zeros or a specified value.

Parameters:
  • array – The N-dimensional array to shift.

  • offset – A list or tuple of length N of offsets in the same order as the array indices. A positive offset shifts to higher numbers on the axis.

  • fill – The value used to fill the newly created array elements.

Returns:

a zero offset returns the same array object (no copy); any non-zero offset returns a fresh copy. Do not mutate the result in place assuming it is always independent of the input.

Return type:

The array shifted by the given amount. Aliasing note

Raises:

ValueError – If the array shape and offset list have different lengths.

unpad_array(array: NDArray[NPType], margin: list[int] | tuple[int, ...]) NDArray[NPType][source]

Remove a padded margin from each edge.

Parameters:
  • array – The N-dimensional array to unpad.

  • margin – A list or tuple of length N of margin amounts in the same order as the array indices. Each axis is unpadded to its original size minus 2*N and the original centered values are used on the new axis.

Returns:

this never copies – an all-zero margin returns the same array object and a non-zero margin returns a view (slice) into the input. Mutating the result therefore mutates the input; copy first if independence is needed.

Return type:

The array unpadded by the given amount. Aliasing note

Raises:

ValueError – If the array shape and margin list have different lengths.

Global image-quality helpers used during orchestrator preflight.

Three image-level checks happen before any technique runs. This module owns the implementations:

  • saturation_mask: pixels at or above the per-instrument full-well DN.

  • cosmic_ray_mask: single-pixel spikes above 5 sigma over a median-3x3 neighborhood.

Each helper operates on the entire image and produces a boolean mask consumed by feature extractors and the gradient computation. Image-side operations are global by design — no helper here crops to a predicted position.

cosmic_ray_mask(image: NDArray[floating[Any]], *, image_noise_sigma: float, k_sigma: float = 5.0) NDArray[bool][source]

Boolean mask flagging single-pixel spikes above a sigma threshold.

Compares each pixel against a 3x3 median around it (reflect boundary handling); pixels whose excess over the local median exceeds k_sigma * image_noise_sigma are flagged. Suitable as a cheap pre-detection step that prevents single hot pixels from masquerading as star detections in pattern matching.

Parameters:
  • image – 2-D float input array.

  • image_noise_sigma – Global MAD-based noise sigma (DN units); typically from estimate_image_noise_sigma.

  • k_sigma – Multiplier on image_noise_sigma (default 5).

Returns:

Boolean mask of the same shape as image.

Raises:
saturation_mask(image: NDArray[floating[Any]], *, full_well_dn: float) NDArray[bool][source]

Boolean mask with True where pixels are at or above the saturation DN.

Parameters:
  • image – 2-D float input array.

  • full_well_dn – Per-instrument saturation DN. Pixels at or above this value are flagged as saturated.

Returns:

Boolean mask of the same shape as image.

Raises:

TypeError – if image is not 2-D.

current_git_version() str[source]

Return the git version of the current repo, caching the result.

The result is cached because the git version cannot change during a single program run.

Returns:

The git describe string, or ‘GIT DESCRIBE FAILED’ if the command fails.

dec_rad_to_dms(dec: float) str[source]

Converts declination in radians to a formatted string in degrees, minutes, and seconds.

Parameters:

dec – Declination value in radians.

Returns:

Formatted string in the form “+/-DDDdMMmSS.SSSs”.

flatten_list(lst: list[Any]) list[Any][source]

Flattens a list of lists into a single list.

Parameters:

lst – The list to flatten.

Returns:

A flattened list.

get_local_host_name() str[source]

Return this machine’s fully qualified domain name as a string.

The value is obtained from socket.getfqdn() on the first call and stored in the module-level _LOCAL_HOST_NAME_CACHE so later calls return the same string without calling getfqdn again.

Returns:

The FQDN string on success, or the literal 'LOCAL HOST NAME FAILED' if socket.getfqdn() raises any exception.

Side effects:

On the first successful call, sets _LOCAL_HOST_NAME_CACHE to the FQDN. On the first failing call, sets _LOCAL_HOST_NAME_CACHE to 'LOCAL HOST NAME FAILED'. Subsequent calls return the cached value and do not call socket.getfqdn() again.

log_run_environment(logger: PdsLogger, command_list: list[str]) None[source]

Log host, git, and command-line context to the given logger.

Call once at process startup on the main logger (e.g. sd_mosaic after setup_logging). Per-image loggers may omit this to avoid duplicating the same block on the console when handlers mirror output to MAIN_LOGGER.

Parameters:
  • logger – The logger to write to.

  • command_list – The command-line arguments for the current run (typically sys.argv[1:]).

mad_std(a: NDArray[floating[Any]] | list[float]) float[source]

Median absolute deviation (MAD) standard deviation.

Parameters:

a – Sample values; must contain at least one finite element.

Returns:

The robust standard-deviation estimate 1.4826 * MAD.

Raises:

ValueError – If a is empty or contains no finite values, which would otherwise return a silent NaN that poisons every downstream covariance / confidence computation.

ra_rad_to_hms(ra: float) str[source]

Converts right ascension in radians to a formatted string in hours, minutes, and seconds.

Parameters:

ra – Right ascension value in radians.

Returns:

Formatted string in the form “HHhMMmSS.SSSs”.

Raises:

ValueError – If the right ascension value is negative.

safe_lstrip_zero(s: str) str[source]

Strips leading zeros from a string but leaves one zero behind if that’s all there is.

Parameters:

s – The string to strip leading zeros from.

Returns:

The string with leading zeros stripped.

class NavBase(*, config: Config | None = None, **kwargs: Any)[source]

Bases: object

Provides a base class with configuration and logging capabilities for navigation components.

Serves as the foundation for navigation-related classes by providing common functionality for configuration management and logging.

Parameters:

config – Configuration object for this instance. Uses DEFAULT_CONFIG if not provided.

property config: Config

Returns the configuration object associated with this instance.

property logger: PdsLogger

Returns the logger instance associated with this object.

Robust per-image noise estimate over the sensor area.

The autonomous-navigation orchestrator computes image_noise_sigma once per image and stores it on NavContext so every extractor and technique uses the same value. This module owns the implementation.

The estimate is global – it does not require knowledge of where any feature lives in the image, and is therefore not biased by a wrong SPICE pointing prediction. It is computed from a 3x3 second-difference (Laplacian) response so that smooth scene structure (ring brightness ramps, limb shading, a bright extended disc) cancels and only pixel-to-pixel noise survives; the MAD over that response further rejects the minority of pixels sitting on sharp edges or cosmic rays. A plain MAD of the raw intensities is not used: when the scene is dominated by structure (for example rings filling the frame) it measures the bright-to-dark spread rather than the noise and overestimates sigma by orders of magnitude, which in turn pushes the edge-detection threshold above every real gradient and empties the distance transform.

estimate_background_and_sky_sigma(image: NDArray[floating[Any]], valid_mask: NDArray[bool], *, noise_sigma: float, clip_sigma: float = 3.0) tuple[float, float][source]

Estimate the frame’s background pedestal and sky-noise sigma.

Returns two frame-global quantities body-brightness photometry needs (the BODY_BLOB centroid moment in BodyBlobNav and the BODY_BLOB detection SNR in NavModelBodyBase share this estimate):

  • Background (bias + dark) pedestal. Real raw frames carry a non-zero bias/dark pedestal, and the sim adds a bias_dn pedestal so dark sky is distinguishable from the missing-data marker. That pedestal sits on every pixel, so without subtracting it a brightness-weighted moment is pulled toward the bbox geometric center – a bias that grows with phase as the lit signal concentrates away from center.

  • Sky-noise sigma. Lit-pixel thresholds must reject sky noise, but the global image_noise_sigma is a MAD over the whole sensor, which a bright body inflates (its brightness gradient widens the global spread). An inflated sigma raises the threshold and cuts the dim crescent. The sky noise estimated here, over the body-excluded sky population, is the correct floor; for a small body it equals the global sigma, so this only matters when a large body would otherwise inflate it.

Both are estimated over the valid sky region (sensor data, excluding saturation / cosmic rays) by seeding at the overall median and the global sigma, then rejecting the bright body (the high tail) and re-estimating the median and MAD of the surviving sky population. On a zero-background fixture this returns (~0, ~0).

Parameters:
  • image – The extended-FOV image (native units).

  • valid_maskTrue where the pixel is real sensor data and not saturated / a cosmic ray (the sky-candidate population).

  • noise_sigma – Global per-pixel image noise sigma (native units); seeds the sky-population selection.

  • clip_sigma – High-tail rejection threshold in sigmas; pixels above background + clip_sigma * sky_sigma are excluded from the sky population on each iteration.

Returns:

(background, sky_sigma) in the image’s native units; (0.0, max(noise_sigma, 1e-9)) when there are no valid pixels.

estimate_image_noise_sigma(image: NDArray[floating[Any]], sensor_mask: NDArray[bool] | None = None) float[source]

Return a robust noise sigma over the sensor pixels.

The estimate is the MAD-based standard deviation of a 3x3 second-difference (Laplacian) response, divided by 6. The second difference cancels smooth brightness structure so the value reflects pixel-to-pixel noise rather than scene content, and the MAD rejects the minority of pixels on sharp edges or cosmic rays.

Parameters:
  • image – 2-D float input array.

  • sensor_mask – Optional boolean mask with True for sensor pixels and False for extfov padding. If None, every pixel of image is treated as sensor data. Only response pixels whose full 3x3 neighbourhood lies inside the mask contribute.

Returns:

Robust noise sigma in the same DN units as image.

Raises:

NavStatusReason — typed enumeration of every navigation outcome reason.

A single NavStatusReason value is attached to every NavResult produced by the orchestrator. The value tells callers, in one slot, exactly why the result has its status — success modes, image-quality refusals, kernel problems, ensemble disagreement, and degenerate-geometry refusals.

Located under spindoctor.support (not spindoctor.nav_orchestrator) because some values describe failure modes that are not strictly navigation outcomes — image-load errors, missing kernels, instrument-not-configured — and are produced by code outside the orchestrator (image-quality classifier, kernel-loading shim, dataset enumeration).

class NavStatusReason(*values)[source]

Bases: StrEnum

Discrete outcome reasons set on every NavResult.

The value tells downstream consumers exactly why the navigation has its final status. Each value maps to a specific outcome:

  • OK: normal success.

  • RANK_1_ONLY: success with only one observable axis (e.g. flat-ring scene with no orthogonal feature); sigma_along_unobservable_px is set on the result.

  • CONFLICTED_TECHNIQUES: multiple agreement groups exist after grouping and the best-vs-runner-up summed-confidence gap is below agreement_gap; offset reported with reduced confidence.

  • NO_SIGNAL_IN_IMAGE: image classifier flagged a blank or dark frame.

  • IMAGE_OVEREXPOSED: image classifier saw most pixels at full-well DN.

  • MISSING_DATA_DOMINANT: image classifier saw too many missing-data pixels.

  • IMAGE_CORRUPT: image file failed to parse or read.

  • KERNELS_UNAVAILABLE: SPICE coverage missing for the image ET.

  • INSTRUMENT_NOT_CONFIGURED: no per-instrument YAML block for this camera.

  • TITAN_UNSUPPORTED: the only navigable content in the frame is Titan, whose opaque haze hides the surface, so no shape-based or haze-limb navigation is available; the pipeline records the refusal instead of a silent empty failure.

  • NO_FEATURES_EXTRACTED: every extractor returned an empty list.

  • ALL_FEATURES_GATED: features extracted but every one fell below the reliability gate.

  • NO_FEASIBLE_TECHNIQUES: features pass the gate but no technique’s is_feasible returned true.

  • ALL_TECHNIQUES_SPURIOUS: every technique returned spurious=True.

  • FINAL_CONFIDENCE_BELOW_THRESHOLD: ensemble combined confidence sat below min_confidence (or below every tier’s min_confidence).

  • FINAL_SIGMA_ABOVE_THRESHOLD: combined confidence cleared the lowest tier’s min_confidence but the offset sigma exceeded every tier’s max_sigma_px (confident but too imprecise to earn any tier).

  • UNOBSERVABLE_OFFSET: every input covariance shares one null direction; the precision-weighted combine cannot proceed.

  • CONTRACT_VIOLATION: an internal navigation invariant was violated (NavContractError); a programming error upstream, not bad image data. The full traceback is in the error log.

ALL_FEATURES_GATED = 'all_features_gated'
ALL_TECHNIQUES_SPURIOUS = 'all_techniques_spurious'
CONFLICTED_TECHNIQUES = 'conflicted_techniques'
CONTRACT_VIOLATION = 'contract_violation'
FINAL_CONFIDENCE_BELOW_THRESHOLD = 'final_confidence_below_threshold'
FINAL_SIGMA_ABOVE_THRESHOLD = 'final_sigma_above_threshold'
IMAGE_CORRUPT = 'image_corrupt'
IMAGE_OVEREXPOSED = 'image_overexposed'
INSTRUMENT_NOT_CONFIGURED = 'instrument_not_configured'
KERNELS_UNAVAILABLE = 'kernels_unavailable'
MISSING_DATA_DOMINANT = 'missing_data_dominant'
NO_FEASIBLE_TECHNIQUES = 'no_feasible_techniques'
NO_FEATURES_EXTRACTED = 'no_features_extracted'
NO_SIGNAL_IN_IMAGE = 'no_signal_in_image'
OK = 'ok'
RANK_1_ONLY = 'rank_1_only'
TITAN_UNSUPPORTED = 'titan_unsupported'
UNOBSERVABLE_OFFSET = 'unobservable_offset'

Annotated-summary-PNG rendering shared by the autonomous and manual paths.

The autonomous pipeline (spindoctor.navigate_image_files._write_summary_png) and the manual-navigation dialog both produce a labelled overlay PNG of the source image with each NavModel’s annotation drawn on top. The rendering logic lives here so both code paths produce visually identical PNGs from the same (obs, annotations, offset_px) triple.

grayscale_to_rgb_with_quantile_stretch(image: NDArray[floating[Any]]) NDArray[uint8][source]

Build a uint8 RGB grayscale background from a float image.

The black point is fixed at the 0.001 quantile. The white point adapts to the number of “bright” pixels in the image: the default 0.999 quantile clips the top 0.1 % of pixels, but on an image with only a handful of bright outliers (a sparse star field over dark sky, a distant body against empty sky) that fixed clip count saturates every bright pixel to 255 even though the brightest is much brighter than the rest.

The fix counts the bright outliers via a robust median + 15 * MAD threshold and clips at most half of them — so the brightest few are saturated but the remaining bright pixels keep their relative brightness ordering. When the image carries many bright pixels (a body filling the FOV, a busy ring scene) the original 0.1 % behavior dominates and nothing about the existing visualization changes.

render_annotated_summary_rgb(obs: ObsSnapshot, annotations: Annotations, offset_px: tuple[float, float] = (0.0, 0.0)) NDArrayUint8Type[source]

Composite obs.data with annotations.combine at offset_px.

Builds a quantile-stretched grayscale background from the FOV image, asks the annotations layer for its FOV-shaped RGB overlay at the requested offset, and replaces every pixel where the overlay carries any non-zero color channel. When the annotations collection is empty, the returned RGB is the source-image grayscale alone — so the result is always a faithful record of what the navigator saw.

Parameters:
  • obs – Observation snapshot supplying the background image.

  • annotations – Merged Annotations collection from every NavModel that contributed.

  • offset_px(dv, du) offset that shifts the overlay onto the best-fit pose. The convention matches every other offset in the pipeline: predicted + offset = actual.

Returns:

(H, W, 3) uint8 RGB array in FOV coordinates.

dt_delta_str(start_time: datetime, end_time: datetime) str[source]

Returns the difference between two datetime objects as a string representation.

Parameters:
  • start_time – The starting datetime.

  • end_time – The ending datetime.

Returns:

String representation of the time difference.

et_to_utc(et: float, digits: int = 3) str[source]

Returns the UTC time for a given ET time.

Parameters:
  • et – The SPICE ET time (equivalent to TDB).

  • digits – The number of digits to include after the decimal point.

Returns:

The UTC time as a string.

now_dt() datetime[source]

Returns the current time as a datetime object with timezone information.

Returns:

Current time as a timezone-aware datetime object.

now_iso() str[source]

Returns the current time as an ISO 8601 formatted string with timezone information.

Returns:

Current time as an ISO 8601 formatted string.

utc_to_et(utc: str) float[source]

Returns the ET time (TDB seconds) for a given UTC time string.

Parameters:

utc – The UTC time as an ISO 8601 formatted string (e.g., “2008-01-01 12:00:00” or “2008-01-01T12:00:00”).

Returns:

The SPICE ET time (equivalent to TDB) in seconds as a float.

Shared type aliases and protocols used across the nav package.

This module defines the numpy-array aliases (NDArrayBoolType, NDArrayFloatType, NDArrayIntType, NDArrayUint8Type, NDArrayUint32Type, NDArrayType), the generic NPType type variable, the PathLike union accepted by I/O helpers, and the MutableStar protocol describing the in-memory star-record shape used by the star-catalog reduction code.

Centralising these aliases keeps every import site aligned on a single spelling for the heavily-used numpy types and lets a downstream module narrow them in one place.

class MutableStar(*args, **kwargs)[source]

Bases: Protocol

b_v: float | None
catalog_name: str
conflicts: str
dec: float | None
dec_pm: float
diff_u: float
diff_v: float
dn: float
johnson_mag_b: float | None
johnson_mag_faked: bool
johnson_mag_v: float | None
move_u: float
move_v: float
name: str
pretty_name: str
psf_size: tuple[int, int]
ra: float | None
ra_dec_with_pm(tdb: float) tuple[float, float] | tuple[None, None][source]
ra_pm: float
spectral_class: str | None
temperature: float | None
temperature_faked: bool
u: float
unique_number: int | None
v: float
vmag: float | None