spindoctor.nav_technique
NavTechnique base class for the autonomous-navigation pipeline.
Concrete subclasses self-register via __init_subclass__ and the
orchestrator iterates the registry. Every concrete technique is safe to
instantiate per-image without depending on prior runs;
__init_subclass__ only records a class reference — no instances are
constructed at import time.
- class NCCCovarianceTuning(localization_uncertainty_scale: float, model_error_size_frac: float, model_error_floor_px: float)[source]
Bases:
objectModel-error terms folded into an NCC technique’s translation covariance.
The bare peak-curvature (matched-filter) covariance measures only the statistical precision at the correlation peak. On a well-matched template that precision is far smaller than the true registration error, which is dominated by silhouette / photometric model error and by localization ambiguity. Three quadrature terms restore calibration:
localization_uncertainty_scalemultiplies the inter-pyramid-level peak migration (applied inside the correlator, carried here for completeness of the covariance model).model_error_size_fracscales a body / template size in pixels into a size-proportional silhouette-error sigma (a coherent shape mismatch displaces the peak by a roughly fixed fraction of the extent).model_error_floor_pxis a size-independent absolute floor (pointing, camera distortion, sub-pixel interpolation).
- Parameters:
localization_uncertainty_scale – Multiplier on the correlator
consistency(px);0.0disables it.model_error_size_frac – Fraction of the template extent (px) taken as the silhouette-error sigma;
0.0disables it.model_error_floor_px – Absolute floor (px);
0.0disables it.
- class NavTechnique(*, config: Config | None = None, **kwargs: Any)[source]
-
Abstract base for autonomous-navigation techniques.
Concrete subclasses self-register via
__init_subclass__. The orchestrator iteratesNavTechnique._registryto discover the techniques it should run. Subclasses must override the class attributesname,accepts_feature_types, and (when relevant)requires_prior.- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- confidence_spec: ClassVar[ConfidenceSpec | None] = None
Confidence-formula spec consumed by
evaluate_sigmoid_combination. Loaded fromconfig_510_techniques.yamland assigned atConfig.read_configtime.Nonefor techniques that opt out of the autonomous registry (e.g.NavTechniqueManual).
- abstractmethod is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Return whether this technique can run on the supplied features.
is_feasiblereads feature metadata only — never pixels. It is called beforenavigateand short-circuits work that has no chance of producing a useful result.- Parameters:
features – Full feature set after the reliability gate. The technique filters to its accepted types internally.
- Returns:
A
NavFeasibilityReportwhosefeasiblefield tells the orchestrator whether to invokenavigate.
- abstractmethod navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Compute and return a single per-technique offset estimate.
- Parameters:
features – Features filtered to this technique’s accepted types (the orchestrator pre-filters; the technique need not re-filter unless an internal sub-check requires it).
context – Per-image NavContext with image, masks, and shared derivatives.
- Returns:
A
NavTechniqueResultwith offset, covariance, confidence, and per-technique diagnostics.
- requires_prior: ClassVar[bool] = False
If
True, this technique requires a prior offset onNavContextand is run only on pass 2.
- tier: ClassVar[Literal['primary', 'fallback']] = 'primary'
Technique tier consumed by the ensemble.
'primary'techniques always vote;'fallback'techniques are dropped when any non-spurious primary result exists for the same source body (matched by body name extracted fromfeature_id). The classic example isBodyTerminatorNav(fallback) being superseded by a non-spuriousBodyLimbNavorBodyDiscCorrelateNavresult on the same body — limb / disc are geometric features whose failure modes are visibility- driven, while the terminator is a photometric feature that can mis-converge on textured surfaces with no per-technique signal to detect the failure.
- tuning: ClassVar[dict[str, float | int]] = {}
Per-technique runtime tuning loaded from
config_510_techniques.yaml.techniques.<name>.tuning. Each technique pulls the values it needs by name from this dict and falls back to the module-level default constant when a key is missing. Empty for techniques that opt out of the autonomous registry or that have no tunable parameters.
- ROTATION_AT_EDGE_FRACTION: float = 0.95
Default fraction of
max_rotation_degat which the rotation parameter trips the technique’sat_edgeflag.When the converged
|theta|exceedsrotation_at_edge_fraction * max_rotation_degthe LM (or Procrustes / NCC pyramid) is reporting a rotation right against the configured cap, which usually signals that the cap is too tight or the geometry is unobservable in the rotation direction.Each 3-DoF technique reads its threshold from
config_510_techniques.yamlundertechniques.<name>.tuning.rotation_at_edge_fraction; this constant is the documented default that ships in the YAML. Re-exported so tests and downstream tools can reference the canonical default without re-reading the YAML.
- ROTATION_UNOBSERVABLE_VARIANCE: float = 1000000000000000.0
Finite sentinel used as the rotation-axis variance when a technique provides no rotation evidence (centroid-based fits, single-star matches, template NCC without an outer rotation search).
The pseudoinverse used by the ensemble combine maps this huge variance to a near-zero information contribution in the rotation direction, so the technique contributes no rotation constraint to the precision-weighted merge. A literal
+infwould break covariance validation (np.linalg.eigvalshreturns NaN for non-finite matrices), so this finite sentinel is preferred.
- add_model_error_floor(covariance: NDArray[floating[Any]], floor_px: float) → NDArray[floating[Any]][source]
Return
covariancewithfloor_px**2added to the translation diagonal.Shared by every technique that applies the #210 model-error floor, so the quadrature-sum convention and its rationale live in one place (see
load_model_error_floor()). The input is not mutated.- Parameters:
covariance –
(2, 2)or(3, 3)covariance; only the leading two diagonal entries (the translation axes) are floored.floor_px – Validated floor in pixels;
0.0returns the input unchanged.
- Returns:
The floored covariance (a copy when
floor_px > 0).
- add_size_scaled_model_error(covariance: NDArray[floating[Any]], *, size_px: float, size_frac: float, floor_px: float) → NDArray[floating[Any]][source]
Add a size-proportional silhouette error and an absolute floor in quadrature.
Adds
(size_frac * size_px)**2 + floor_px**2to the leading two (translation) diagonal entries. The size term makes the reported sigma track body / template extent – a large body’s silhouette mismatch displaces the correlation peak by more pixels than a small body’s – so a single constant floor no longer has to cover the whole size range. The input is not mutated; the localization-spread term is applied separately inside the correlator.- Parameters:
covariance –
(2, 2)or(3, 3)covariance; only the translation diagonal is modified.size_px – Template extent in pixels (body diameter, ring radial span).
size_frac – Silhouette-error fraction of
size_px.floor_px – Absolute floor in pixels.
- Returns:
The inflated covariance (a copy when any term is positive).
- embed_rotation_unobservable(cov_2x2: NDArray[floating[Any]]) → NDArray[floating[Any]][source]
Promote a 2x2 translation covariance to a 3x3 with rotation unobservable.
The third diagonal carries
ROTATION_UNOBSERVABLE_VARIANCE; the cross-covariance entries are zero. The ensemble’s pseudo-inverse combine treats the rotation eigenvalue as null on input from this technique, so the rotation direction is ignored when fusing with rotation-aware techniques.- Parameters:
cov_2x2 – 2x2 translation covariance.
- Returns:
(3, 3)covariance matrix with the original 2x2 in the top-left block and the rotation-unobservable sentinel on the bottom-right diagonal.- Raises:
ValueError – if
cov_2x2is not a 2x2 ndarray.
- filter_technique_names(names: list[str], patterns: str | list[str]) → list[str][source]
Return
namesfiltered by gitignore-style glob patterns.A leading
!marks an exclusion pattern; otherwise the pattern is inclusion. An exclusion-only pattern list implies an inclusion default of'*'.- Parameters:
names – List of technique names (or any candidate strings).
patterns – Single pattern or list of patterns. Patterns matching shell-glob syntax (
*,?,[abc]). Leading!means exclusion.
- Returns:
Names that match at least one inclusion pattern and no exclusion patterns.
- Raises:
ValueError – if an empty pattern list is supplied.
- load_model_error_floor(tuning: dict[str, Any], technique_name: str) → float[source]
Read and validate a technique’s
model_error_floor_pxtunable.The floor is added in quadrature to the technique’s reported translation covariance (#210): robust-fit and NCC covariances measure statistical precision only and under-report the model error the fit cannot see (silhouette mismatch, photometric error, template pivot), leaving those results over-weighted in the ensemble.
- Parameters:
tuning – The technique’s YAML
tuningmapping.technique_name – Technique name for the error message.
- Returns:
The floor in pixels;
0.0(disabled) when the key is absent.- Raises:
ValueError – If the configured value is negative or non-finite (a negative or NaN floor would silently disable the quadrature sum; an infinite floor would poison the covariance and tier gates).
- load_ncc_covariance_tuning(tuning: dict[str, Any], technique_name: str) → NCCCovarianceTuning[source]
Read and validate an NCC technique’s covariance model-error tunables.
- Parameters:
tuning – The technique’s YAML
tuningmapping.technique_name – Technique name for error messages.
- Returns:
The validated
NCCCovarianceTuning.- Raises:
ValueError – If any term is negative or non-finite.
- log_confidence_breakdown(logger: Any, breakdown: ConfidenceBreakdown, *, low_threshold: float = 0.1) → None[source]
Emit a human-readable per-term explanation of a confidence value.
Always logs the breakdown at DEBUG. When the confidence falls below
low_thresholdthe breakdown is also emitted at INFO so an operator running at the default INFO level sees why a fit reported near-zero confidence (typically: a single un-divided term has driven the sigmoid argument to a large negative value).- Parameters:
logger – A pdslogger compatible with
info(...)/debug(...).breakdown – The
ConfidenceBreakdownreturned byevaluate_sigmoid_combination(..., return_breakdown=True).low_threshold – Confidence at or below this value triggers the promotion from DEBUG to INFO.
- rotation_pivot_distance_px(pivot_vu: tuple[float, float], image_shape_vu: tuple[int, int]) → float[source]
Return the rotation-pivot’s distance from the image centre.
Used by 3-DoF DT-fit techniques to convert the LM rotation step into a pixel-equivalent value for the convergence test.
- Parameters:
pivot_vu – Pivot coordinates
(v, u)in image pixels.image_shape_vu – Image shape
(height, width)in V/U pixels.
- Returns:
Euclidean distance from
pivot_vuto the image centre, floored at1.0so a pivot landing exactly on the image centre still yields a non-zero convergence threshold.
- rotation_unobservable_sigma_rad() → float[source]
Return the rotation sigma corresponding to the unobservable sentinel.
Convenience for techniques that report
sigma_rotation_radon theirNavTechniqueResultwhen rotation is unconstrained.- Returns:
sqrt(ROTATION_UNOBSERVABLE_VARIANCE)(radians, as a float).
- search_window_for_obs(context: NavContext) → tuple[int, int][source]
Return the
(margin_v, margin_u)extfov margin from the obs.Every translation-fit technique reads the per-instrument extfov margin from
context.obs.extfov_margin_vuto bound its at-edge detection. Centralising the read in one helper keeps the convention uniform: a test obs stand-in that omits the attribute surfaces anAttributeErrorrather than silently falling through to a fabricated default.
- technique_tier(technique_name: str) → str[source]
Look up a registered technique’s
tierfrom the registry.Returns
'primary'for an unregistered name so unknown techniques (test stubs, future plug-ins) are treated as primary by construction; an explicittier='fallback'declaration is required to opt into the fallback-drop behaviour. Shared by the orchestrator’s fallback pre-filter and the ensemble’s fallback-supersession pass so the tier rule lives in one place.
- validate_registered_confidence_specs() → None[source]
Validate every registered
NavTechnique’s confidence spec.Each technique whose spec was loaded from
config_510_techniques.yaml(assigned byspindoctor.config.config.Config._validate_registered_techniques()) must declare the full set of valid attribute names inconfidence_attributes. Every term’sfeatureand everyhard_zero_ifkey must appear in that set; otherwise the technique would raise at navigate time. Validation runs at config-load time so the failure surfaces during process startup rather than mid-image. Techniques whose spec isNone(test- only registry entries opted out of YAML lookup) are skipped.- Raises:
ValueError – if any term references an unknown attribute. The message names both the technique class and the bad attribute.
NavFeasibilityReport — what a NavTechnique reports about its applicability.
Each technique’s is_feasible(features) returns one of these reports. The
orchestrator consults the report before invoking navigate; infeasible
techniques are skipped silently with their reason recorded for diagnostics.
- class NavFeasibilityReport(feasible: bool, reason: str, consumed_feature_count: int = 0)[source]
Bases:
objectOutcome of a technique’s feasibility check.
- Parameters:
feasible – True if the technique can run on the supplied feature set.
reason – Human-readable text; required when
feasibleis False and ignored when True. Stable wording so the orchestrator can correlate similar refusals across images.consumed_feature_count – Number of features the technique would consume if invoked (after its own type filter). Used by diagnostics; safe to set to 0 when feasible is False.
- Raises:
TypeError – if any field has the wrong type.
ValueError – if
consumed_feature_countis negative orfeasible=Falseis supplied with an emptyreason.
NavTechniqueResult — the per-technique output consumed by the ensemble.
- class NavTechniqueResult(technique_name: str, feature_ids: tuple[str, ...], offset_px: tuple[float, float], covariance_px2: NDArray[floating[Any]], confidence: float, spurious: bool, at_edge: bool, diagnostics: BodyDiscDiagnostics | BodyLimbDiagnostics | BodyTerminatorDiagnostics | BodyBlobDiagnostics | ManualNavDiagnostics | RingEdgeDiagnostics | RingAnnulusDiagnostics | StarFieldDiagnostics | StarUniqueMatchDiagnostics | StarRefineDiagnostics, rotation_rad: float | None = None, sigma_rotation_rad: float | None = None, source_bodies: frozenset[str] = frozenset({}), prior_source_techniques: frozenset[str] = frozenset({}))[source]
Bases:
objectOne per-technique navigation result.
- Parameters:
technique_name – Class name of the producing technique.
feature_ids – Tuple of NavFeature.feature_id values actually consumed. Stored as an immutable
tuple[str, ...]so the hash is stable across the lifetime of the instance; passing alistis accepted and converted in__post_init__.offset_px –
(dv, du)translational offset. Convention: predicted position(v, u)means actual position is(v + dv, u + du).covariance_px2 – 2x2 (or 3x3 with rotation) covariance matrix in pixel^2 (or pixel^2 / radian^2) units.
confidence – Self-assessed [0, 1] calibrated confidence score.
spurious – Hard-reject flag; the ensemble drops spurious results unconditionally.
at_edge – True if the solution touched the search-window boundary.
diagnostics – Per-technique typed diagnostics dataclass.
rotation_rad – Optional fitted camera rotation (radians);
Noneon instruments where rotation fitting is disabled.sigma_rotation_rad – Optional 1-sigma uncertainty on the fitted rotation.
source_bodies – SPICE body names this result was computed against (from each consumed feature’s structured
body_name). Empty for ring / star techniques. The ensemble’s fallback-supersession filter reads this instead of parsingfeature_idsstrings.prior_source_techniques – Technique names whose pass-1 results seeded the prior this (pass-2) result refined; empty for prior-free results. Stamped by the orchestrator after the pass-2 run. The ensemble treats such a result as conditionally dependent on these techniques: it may refine their offset but not corroborate it.
Per-technique diagnostics dataclasses.
Each NavTechnique returns a typed diagnostics object on its
NavTechniqueResult.diagnostics field. The curator walks
CURATOR_FIELDS on each diagnostics class to decide which fields land in
the JSON metadata; an unmapped field is a CI failure.
- class BodyBlobDiagnostics(body_snr_inside_predicted_bbox: float = 0.0, body_extent_px: float = 0.0, blob_count: int = 0, residual_px: float = 0.0, max_phase_angle_deg: float = 0.0, max_phase_irregularity_factor: float = 0.0)[source]
Bases:
objectDiagnostics emitted by
BodyBlobNav.- Parameters:
body_snr_inside_predicted_bbox – SNR within the predicted bbox.
body_extent_px – Predicted body’s longer-axis extent in pixels.
blob_count – Number of BODY_BLOB features fused.
residual_px – Centroid-fit RMS residual.
max_phase_angle_deg – Maximum raw phase angle across the consumed blobs. Recorded for diagnostic inspection only; the confidence formula consumes
max_phase_irregularity_factorinstead because raw phase understates the centroid uncertainty for an irregular body.max_phase_irregularity_factor – Maximum
(ellipsoid_rms_residual_km / body_radius_km) * (1 + 2 * sin(phase/2)**2)across the consumed blobs. The confidence formula uses this term to down-weight irregular high-phase scenes where the lit-weighted predicted centroid cannot fully correct for the unknown-orientation shadowing on a non-ellipsoidal body.
- CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'blob_count': 'blob_count', 'body_extent_px': 'body_extent_px', 'body_snr_inside_predicted_bbox': 'body_snr_inside_predicted_bbox', 'max_phase_angle_deg': 'max_phase_angle_deg', 'max_phase_irregularity_factor': 'max_phase_irregularity_factor', 'residual_px': 'residual_px'}
- class BodyDiscDiagnostics(ncc_peak: float = 0.0, peak_to_runner_up_ratio: float = 0.0, consistency_px: float = 0.0, consistency_ratio: float = 0.0, used_gradient: bool = False, body_count: int = 0)[source]
Bases:
objectDiagnostics emitted by
BodyDiscCorrelateNav.- Parameters:
ncc_peak – Peak normalized cross-correlation value.
peak_to_runner_up_ratio – Ratio of NCC peak to second-highest peak outside the exclusion radius around the peak.
consistency_px – Inter-pyramid peak migration in pixels (raw).
consistency_ratio –
consistency_pxdivided by the per-image spurious threshold (which scales with body diameter). A value <= 1.0 means the result is within the diameter-scaled consistency budget; the confidence formula consumes this normalized form so a healthy fit on a large body is not penalized by the same divisor that’s appropriate for a small body.used_gradient – True if gradient mode was selected by
auto.body_count – Number of BODY_DISC features fused into the combined template.
- class BodyLimbDiagnostics(visible_limb_arc_fraction: float = 0.0, visible_arc_px: float = 0.0, dt_fit_rms_px: float = 0.0, lm_iterations: int = 0, tukey_inlier_count: int = 0, lm_converged: bool = True, polarity_rejection_fraction: float = 0.0, coarse_peak_fraction: float = 0.0)[source]
Bases:
objectDiagnostics emitted by
BodyLimbNav.- Parameters:
visible_limb_arc_fraction – Fused visible-arc fraction across input LIMB_ARC features.
visible_arc_px – Total surviving polyline arc length in pixels.
dt_fit_rms_px – Final root-mean-square DT residual.
lm_iterations – Levenberg-Marquardt iteration count.
tukey_inlier_count – Number of polyline vertices accepted by the Tukey biweight robust estimator.
lm_converged – True when the LM met its step-norm tolerance before the iteration cap; False marks an unverified fit whose confidence the shared DT gates cap.
polarity_rejection_fraction – Fraction of model vertices whose local gradient direction disagreed with the model normal at the seed.
coarse_peak_fraction – The winning coarse-NCC shift’s in-bounds match fraction over the RASTERIZED polyline pixels (acquisition quality). Its denominator is mask pixels, not vertices, so it is not directly comparable to
tukey_inlier_count.
- CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'coarse_peak_fraction': 'coarse_peak_fraction', 'dt_fit_rms_px': 'dt_fit_rms_px', 'lm_converged': 'lm_converged', 'lm_iterations': 'lm_iterations', 'polarity_rejection_fraction': 'polarity_rejection_fraction', 'tukey_inlier_count': 'tukey_inlier_count', 'visible_arc_px': 'visible_arc_px', 'visible_limb_arc_fraction': 'visible_limb_arc_fraction'}
- class BodyTerminatorDiagnostics(visible_terminator_arc_fraction: float = 0.0, visible_arc_px: float = 0.0, dt_fit_rms_px: float = 0.0, lm_iterations: int = 0, tukey_inlier_count: int = 0, mean_phase_angle_factor: float = 0.0, mean_albedo_penalty: float = 0.0, secondary_basin_distance_px: float | None = None, secondary_basin_cost_ratio: float | None = None, lm_converged: bool = True, polarity_rejection_fraction: float = 0.0, coarse_peak_fraction: float = 0.0)[source]
Bases:
objectDiagnostics emitted by
BodyTerminatorNav.Parameters: same shape as
BodyLimbDiagnosticswithvisible_terminator_arc_fractionsubstituted, plus the terminator-specific confidence inputs and the basin second-opinion pair:- mean_phase_angle_factor: Vertex-weighted mean
sin(phase) factor across the consumed terminator features. A confidence term (a terminator sharpens with phase), so it is recorded per result for the calibration fit.
- mean_albedo_penalty: Vertex-weighted mean catalog albedo-variation
penalty across the consumed features; likewise a recorded confidence input.
- secondary_basin_distance_px: Distance from the converged offset to
the best competing DT-cost basin in the search window;
Nonewhen the scan did not run or found no eligible shift (0.0is a legitimate measured value, so it is not the sentinel).- secondary_basin_cost_ratio: Competing basin’s mean per-vertex DT
cost divided by the converged cost (epsilon-guarded); values below the technique’s
basin_cost_ratio_thresholdmark the fit spurious.Nonewhen not measured.- lm_converged: Shared DT gate diagnostic; the field carries the
same meaning as on
BodyLimbDiagnostics.- polarity_rejection_fraction: Shared DT gate diagnostic; the field
carries the same meaning as on
BodyLimbDiagnostics.- coarse_peak_fraction: Shared DT gate diagnostic; the field carries
the same meaning as on
BodyLimbDiagnostics.
- CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'coarse_peak_fraction': 'coarse_peak_fraction', 'dt_fit_rms_px': 'dt_fit_rms_px', 'lm_converged': 'lm_converged', 'lm_iterations': 'lm_iterations', 'mean_albedo_penalty': 'mean_albedo_penalty', 'mean_phase_angle_factor': 'mean_phase_angle_factor', 'polarity_rejection_fraction': 'polarity_rejection_fraction', 'secondary_basin_cost_ratio': 'secondary_basin_cost_ratio', 'secondary_basin_distance_px': 'secondary_basin_distance_px', 'tukey_inlier_count': 'tukey_inlier_count', 'visible_arc_px': 'visible_arc_px', 'visible_terminator_arc_fraction': 'visible_terminator_arc_fraction'}
- mean_phase_angle_factor: Vertex-weighted mean
- class ManualNavDiagnostics(operator_accepted: bool = True)[source]
Bases:
objectDiagnostics emitted by
NavTechniqueManual.- Parameters:
operator_accepted –
Truewhen the operator confirmed the dialog’s chosen offset. AlwaysTrueon results that reach the curator (cancelled picks short-circuit before theNavResultis built), but kept explicit so the JSON metadata records the fact that a human, not an autonomous technique, set the offset.
- NavTechniqueDiagnostics = spindoctor.nav_technique.diagnostics.BodyDiscDiagnostics | spindoctor.nav_technique.diagnostics.BodyLimbDiagnostics | spindoctor.nav_technique.diagnostics.BodyTerminatorDiagnostics | spindoctor.nav_technique.diagnostics.BodyBlobDiagnostics | spindoctor.nav_technique.diagnostics.ManualNavDiagnostics | spindoctor.nav_technique.diagnostics.RingEdgeDiagnostics | spindoctor.nav_technique.diagnostics.RingAnnulusDiagnostics | spindoctor.nav_technique.diagnostics.StarFieldDiagnostics | spindoctor.nav_technique.diagnostics.StarUniqueMatchDiagnostics | spindoctor.nav_technique.diagnostics.StarRefineDiagnostics
Sum type spanning every per-technique diagnostics dataclass.
The orchestrator’s curator and the technique-result type both consume this union; adding a new technique means adding both its diagnostics dataclass above and a new entry into this union.
- class RingAnnulusDiagnostics(ncc_peak: float = 0.0, peak_to_runner_up_ratio: float = 0.0, annulus_count: int = 0, used_gradient: bool = False)[source]
Bases:
objectDiagnostics emitted by
RingAnnulusNav.- Parameters:
ncc_peak – Peak NCC value.
peak_to_runner_up_ratio – NCC peak ratio.
annulus_count – Number of RING_ANNULUS features (one per planet).
used_gradient – True if gradient mode was selected.
- class RingEdgeDiagnostics(total_edge_length_px: float = 0.0, per_edge_dt_rms_summed: float = 0.0, per_edge_dt_rms_mean: float = 0.0, per_edge_dt_median_max: float = 0.0, edge_count: int = 0, is_rank_1: bool = False, lm_converged: bool = True, coarse_peak_fraction: float = 0.0, sigma_orbit_radial_px: float = 0.0)[source]
Bases:
objectDiagnostics emitted by
RingEdgeNav.- Parameters:
total_edge_length_px – Cumulative pixel length of all surviving ring-edge polylines.
per_edge_dt_rms_summed – Sum of per-edge final DT RMS values.
per_edge_dt_rms_mean – Mean per-edge final DT RMS value (
per_edge_dt_rms_summed / edge_count). Edge-count independent, so it – not the raw sum – is the scale the confidence formula uses; the sum grows with the number of fused edges and a fixed divisor cannot normalise it.per_edge_dt_median_max – Largest per-edge median absolute DT residual (px). The mis-convergence gate statistic: a wholly misaligned edge (the wrong-ringlet failure mode) drives its own median to the ringlet spacing, while a minority of vertices snapping to a neighbouring parallel edge (routine on flat multi-edge scenes) leaves every median near the fit residual.
edge_count – Number of RING_EDGE features fused.
is_rank_1 – True if every ring-edge feature was straight-line and the combined covariance is rank-1.
lm_converged – Shared DT gate diagnostic; the field carries the same meaning as on
BodyLimbDiagnostics.coarse_peak_fraction – Shared DT gate diagnostic; the field carries the same meaning as on
BodyLimbDiagnostics.sigma_orbit_radial_px – Effective fully-correlated radial orbit-uncertainty sigma (px) added in quadrature to the reported covariance along the fit’s radial direction (the weight-weighted combine of the consumed features’
sigma_orbit_radial_px);0.0when no consumed feature declares an orbit uncertainty.
- CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'coarse_peak_fraction': 'coarse_peak_fraction', 'edge_count': 'edge_count', 'is_rank_1': 'is_rank_1', 'lm_converged': 'lm_converged', 'per_edge_dt_median_max': 'per_edge_dt_median_max', 'per_edge_dt_rms_mean': 'per_edge_dt_rms_mean', 'per_edge_dt_rms_summed': 'per_edge_dt_rms_summed', 'sigma_orbit_radial_px': 'sigma_orbit_radial_px', 'total_edge_length_px': 'total_edge_length_px'}
- class StarFieldDiagnostics(n_inliers: int = 0, median_residual_px: float = 0.0, n_detected_sources: int = 0, n_catalog_predicted: int = 0, n_triplets_evaluated: int = 0, rotation_below_separability_floor: bool = False)[source]
Bases:
objectDiagnostics emitted by
StarFieldFromCatalogNav.- Parameters:
n_inliers – Number of detection-to-catalog inliers after RANSAC.
median_residual_px – Median position residual on inliers.
n_detected_sources – Number of bright sources detected in the image.
n_catalog_predicted – Number of catalog stars in the extfov.
n_triplets_evaluated – Number of triplet candidates considered by RANSAC.
rotation_below_separability_floor – True when a co-fitted camera rotation’s magnitude fell below the roll/translation separability floor (
rotation_separability_floor_deg) and the rotation was therefore reported as unobservable rather than as a confident near-zero value.
- CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'median_residual_px': 'median_residual_px', 'n_catalog_predicted': 'n_catalog_predicted', 'n_detected_sources': 'n_detected_sources', 'n_inliers': 'n_inliers', 'n_triplets_evaluated': 'n_triplets_evaluated', 'rotation_below_separability_floor': 'rotation_below_separability_floor'}
- class StarRefineDiagnostics(n_stars_used: int = 0, median_pos_err_px: float = 0.0, residual_scatter_px: float = 0.0)[source]
Bases:
objectDiagnostics emitted by
StarRefineNav.- Parameters:
n_stars_used – Number of stars that survived per-star quality gates.
median_pos_err_px – Median refinement positional error.
residual_scatter_px – Per-axis RMS scatter of the per-star residuals.
- class StarUniqueMatchDiagnostics(mode: str = '', predicted_snr: float = 0.0, brightness_margin_mag: float = 0.0, residual_px: float = 0.0, detection_peak_ratio: float = 0.0)[source]
Bases:
objectDiagnostics emitted by
StarUniqueMatchNav.- Parameters:
mode –
'one_star'or'two_star'.predicted_snr – Predicted SNR of the brightest catalog star.
brightness_margin_mag – Mag difference to the next-brightest unmatched catalog source predictable in extfov;
+infwhen no unmatched star exists (a 1-star scene with no other predictable star, or a 2-star scene with no third predictable star to compare against).residual_px – Detection-vs-prediction residual.
detection_peak_ratio – Background-subtracted peak-to-runner-up ratio of the 1-star path’s detection inside its search window (
infwhen the runner-up does not clear the background; 0.0 on the 2-star path, which cross-checks via its assignment residual instead).
Shared sigmoid-of-linear-combination confidence formula.
Every NavTechnique converts its NavTechniqueDiagnostics into a
calibrated [0, 1] confidence using the same shape:
confidence = sigmoid(alpha0 + sum_i alpha_i * normalize_i(x_i))
where normalize_i applies the spec’s offset -> divisor -> cap_at
transformation. Hard-zero gates (e.g. at_edge=True) force confidence
to 0; an optional hard_cap clamps the result.
The math lives here so techniques’ YAML formula specs can be evaluated uniformly and a config-load validation pass can verify every spec at startup.
- class ConfidenceBreakdown(confidence: float, sigmoid_arg: float, alpha0: float, terms: tuple[ConfidenceTermContribution, ...], hard_zero: str | None, hard_cap_applied: bool)[source]
Bases:
objectPer-step trace of a sigmoid-combination evaluation.
- Parameters:
confidence – Final
[0, 1]confidence after sigmoid + hard_cap.sigmoid_arg – The argument fed into the sigmoid (sum of alpha0 + term contributions).
alpha0 – The constant baseline term.
terms – Per-term contributions.
hard_zero – Name of the
hard_zero_ifattribute that fired, orNonewhen no hard-zero gate applied.hard_cap_applied – True if the final sigmoid value was clamped by
spec.hard_cap.
- terms: tuple[ConfidenceTermContribution, ...]
- class ConfidenceSpec(alpha0: float, terms: tuple[~spindoctor.nav_technique.confidence.ConfidenceTerm, ...]=(), hard_zero_if: dict[str, bool]=<factory>, hard_cap: float | None = None)[source]
Bases:
objectFull confidence formula spec for one NavTechnique.
- Parameters:
alpha0 – Constant term in the sigmoid argument.
terms – Tuple of
ConfidenceTermlinear contributions.hard_zero_if – Mapping of diagnostic-attribute names (str) to expected boolean values; if any condition holds, confidence = 0.
hard_cap – Optional upper-bound clamp applied after the sigmoid. When set, must lie in
[0, 1].
- Raises:
TypeError – if
alpha0is not numeric,termsis not a tuple ofConfidenceTerm, orhard_zero_ifis not a mapping ofstrtobool.ValueError – if
hard_capis outside[0, 1].
- terms: tuple[ConfidenceTerm, ...] = ()
- class ConfidenceTerm(feature: str, alpha: float, offset: float = 0.0, divisor: float = 1.0, cap_at: float | None = None)[source]
Bases:
objectOne term in the sigmoid-of-linear-combination formula.
- Parameters:
feature – Name of the diagnostic-dataclass attribute supplying the raw value.
alpha – Linear-combination coefficient.
offset – Subtracted from the raw value before scaling.
divisor – Raw value is divided by this after offset (default 1). Must be non-zero.
cap_at – Optional upper bound on the post-scale value (clamped to
[0, cap_at]if set). When set, must lie in[0, 1].
- Raises:
TypeError – if
alpha,offset,divisor, orcap_atis not numeric.ValueError – if
divisoris zero orcap_atis outside[0, 1].
- class ConfidenceTermContribution(feature: str, raw: float, normalized: float, alpha: float, contribution: float)[source]
Bases:
objectOne term’s contribution to the sigmoid argument.
- Parameters:
feature – The diagnostic-attribute name.
raw – The raw value read off the diagnostics object.
normalized –
rawafter offset / divisor / cap_at.alpha – Linear coefficient applied.
contribution –
alpha * normalized— the term’s signed contribution to the sigmoid argument.
- evaluate_sigmoid_combination(spec: ConfidenceSpec, diagnostics: Any, *, technique_name: str = '', return_breakdown: bool = False) → Any[source]
Evaluate the spec’s sigmoid formula against a diagnostics object.
- Parameters:
spec – The technique’s confidence formula spec.
diagnostics – Diagnostics dataclass instance for the technique.
technique_name – Optional human-readable identifier used in error messages.
return_breakdown – When True, return a tuple
(confidence, ConfidenceBreakdown)so callers can log a per-term explanation of low / zero confidence values.
- Returns:
Calibrated confidence in
[0, 1], or(confidence, ConfidenceBreakdown)whenreturn_breakdown=True.- Raises:
ValueError – if a term references an attribute that does not exist on the diagnostics object. The message names the missing attribute and the technique.
Build ConfidenceSpec instances from the techniques config block.
The per-technique confidence-formula coefficients live in
config_files/config_510_techniques.yaml under the techniques key.
Each technique reads its own block via load_confidence_spec(),
which validates the YAML shape and returns a frozen
ConfidenceSpec. Mis-typed numbers, unknown keys, and missing
alpha0 raise at config-load time so a malformed file fails fast at
process startup instead of mid-image.
- exception ConfidenceConfigError[source]
Bases:
ValueErrorRaised when
config_510_techniques.yamlcannot build a valid spec.Always names the technique whose block triggered the failure so a typo or shape error surfaces with full diagnostic at startup.
- load_confidence_spec(techniques: dict[str, Any], technique_name: str) → ConfidenceSpec[source]
Return the
ConfidenceSpecfortechnique_name.Reads the
techniques[technique_name]mapping and constructs aConfidenceSpecfrom its fields. All numeric coercion and range checks happen here so per-technique modules stay free of YAML-shape bookkeeping. Callers typically passConfig.category('techniques')(anAttrDict, which subclassesdict).- Parameters:
techniques – Mapping of
NavTechnique.nameto its raw config block (thetechniquessection ofconfig_510_techniques.yaml).technique_name –
NavTechnique.nameto look up.
- Returns:
A frozen
ConfidenceSpecready to assign to the technique’sconfidence_specclass attribute.- Raises:
ConfidenceConfigError – If the block is missing, has the wrong shape, or carries an unknown / malformed field.
- load_technique_tuning(techniques: dict[str, Any], technique_name: str) → dict[str, float | int][source]
Return the
tuningblock fortechnique_name.Returns the dict-shaped
tuningsub-block as a flat{key: number}mapping with all numeric values coerced tofloatorint(booleans rejected — YAML’s loose numeric types makeTruelook numeric without this guard). Each technique consumer pulls the values it needs by name and supplies its own default for missing keys.- Parameters:
techniques – Mapping of
NavTechnique.nameto its raw config block (thetechniquessection ofconfig_510_techniques.yaml).technique_name –
NavTechnique.nameto look up.
- Returns:
{key: value}or an empty mapping when the technique block has notuningsub-block.- Raises:
ConfidenceConfigError – If the block is malformed or any tuning value is not a finite numeric scalar.
Shared distance-transform fitting machinery for polyline-based techniques.
The body-limb, body-terminator, and ring-edge techniques all follow the same algorithm: render the model polyline as a binary mask, take a coarse 2-D NCC against the image edge mask to get an integer offset, then refine to sub-pixel precision by Levenberg-Marquardt minimisation against the image distance transform with Tukey-biweight outlier rejection. After convergence the M-estimator information matrix is inverted to produce a covariance estimate.
Each helper here is a pure function over numpy arrays; the per-technique classes simply assemble vertices / normals / weights and call into them. The interface is:
coarse_ncc_search()– integer-pixel offset from binary masks.coarse_ncc_search_scored()– the same search, also reporting the winning shift’s match fraction (the acquisition-quality signal).polarity_filter()– per-vertex acceptance from gradient direction.tukey_biweight_weights()– Holland-Welsch redescender weights.lm_subpixel_refine()– translation (or translation + rotation) LM refinement with Tukey reweighting against a precomputed DT.information_matrix_to_covariance()– Hessian to covariance viapinvhso rank-deficient inputs are handled.
The implementation is split by pipeline stage; this module re-exports the
whole surface so consumers import from spindoctor.nav_technique.dt_fitting
regardless of which stage module a helper lives in:
constants– tunable defaults.coarse– rasterization, the integer search, and the polarity classifier.weights– Tukey weighting and the information-matrix inversion.transforms– pose transforms, the DT Jacobian, and the weighted normal equations.ridge– the continuous gradient-ridge polish.lm– the Levenberg-Marquardt driver and its result type.basin– the competing-basin second opinion.
- class CoarseSearchResult(offset_vu: tuple[int, int], score: float)[source]
Bases:
objectStructured output of
coarse_ncc_search_scored().- Parameters:
offset_vu –
(dv, du)integer offset pair at the peak.score – The winning shift’s match fraction – the fraction of its in-bounds RASTERIZED POLYLINE PIXELS landing on edge pixels, in
[0, 1]. Note the denominator counts mask pixels, not model vertices:build_polyline_mask()collapses vertices that round to the same pixel, so this is not directly comparable to a per-vertex count such as a technique’s Tukey inlier count.0.0when the polyline is empty or no candidate shift was eligible. A low score means the coarse acquisition never had a lock: even at its best shift, most of the model found no detected edge underneath it.
- class LMRefineResult(offset_vu: tuple[float, float], rotation_rad: float, covariance: NDArray[floating[Any]], residuals_px: NDArray[floating[Any]], weights: NDArray[floating[Any]], rms_px: float, raw_rms_px: float, iterations: int, converged: bool, inlier_count: int, degenerate: bool, polarity_rejected_count: int = 0)[source]
Bases:
objectStructured output of
lm_subpixel_refine().- Parameters:
offset_vu –
(dv, du)converged translation in pixels.rotation_rad – Converged rotation in radians;
0.0whenfit_rotationwas False.covariance –
(2, 2)or(3, 3)parameter covariance derived from the M-estimator information matrixJ^T diag(w) Jat the converged point. This is the DATA-ONLY covariance: it deliberately EXCLUDES the Tikhonov anchor diagonal thattikhonov_alphaadds to the iteration Hessian, because that anchor is a fitting aid that biases the step rather than measured information and so must not shrink the reported uncertainty.residuals_px –
(N,)per-vertex DT residuals at the final estimate (raw, unweighted).weights –
(N,)per-vertex weights at the final estimate (prior precision times Tukey biweight).rms_px – Weighted root-mean-square of the residuals, computed as
sqrt(sum(w * r**2) / sum(w))over surviving vertices;float('inf')when every vertex was rejected (the degenerate case), so the downstream spurious gates’rms_px > floortest fires instead of reading a zero RMS as a good fit.raw_rms_px – Unweighted root-mean-square of the residuals over the polarity-ACCEPTED vertices, computed as
sqrt(mean(r**2))with no Tukey weighting. Because the Tukey reweighting can down-weight a wholly mis-aligned but polarity-accepted arc to ~0, the weightedrms_pxcollapses to near zero on exactly such a mis-convergence and slips past arms_px > floorgate; the raw RMS retains those Tukey outliers and surfaces the bad fit. Polarity-rejected vertices are excluded (they carry the_INFINITY_DT_PENALTY_PXsentinel and would otherwise dominate the mean);float('inf')when every vertex is polarity-rejected, so the degenerate case still trips the gate.iterations – Number of LM iterations actually performed.
converged – True if the reported pose is LOCALLY converged: either the DT-LM met its step-norm tolerance before the iteration cap, or the final gradient-ridge stage applied and met its own step tolerance (a quantized-DT stall that burns the LM iteration budget at the integer seed is routine on dense edge scenes – the exact condition the ridge stage exists to polish). This certifies local optimality only: the ridge is seeded from the DT-LM pose and bounded by its displacement cap, so it can only polish the SAME lock, and a wrong acquisition whose ridge converges is reported as converged.
Falsemeans neither stage reached a local optimum, which the shared DT fit-quality gates treat as an unverified fit.inlier_count – Number of vertices that retained a strictly positive Tukey weight at the final estimate.
degenerate – True when no vertex survived reweighting (
inlier_count == 0or the surviving weights sum to zero). In this caserms_pxis+infandcovarianceis all-inf; consumers treat it as a spurious fit.polarity_rejected_count – Number of vertices the polarity filter rejected at the seed offset (
0whenuse_polaritywas False).polarity_rejected_count / Nis the polarity-rejection fraction the fit-quality gates consume.
- class RidgeRefineResult(offset_vu: tuple[float, float], rotation_rad: float, iterations: int, converged: bool, applied: bool)[source]
Bases:
objectStructured output of
gradient_ridge_refine().- Parameters:
offset_vu –
(dv, du)refined translation in pixels.rotation_rad – refined rotation in radians (unchanged from the input when
fit_rotationwas False).iterations – Gauss-Newton iterations performed.
converged – True if the step-norm tolerance was met before the cap.
applied – True if the refined pose was accepted. False when the stage found too few valid ridge residuals to fit, or the cumulative displacement from the input offset exceeded the displacement cap – in both cases
offset_vu/rotation_radequal the inputs unchanged and the caller keeps the DT-LM pose.
- class SecondaryBasin(offset_vu: tuple[int, int], distance_px: float, cost_px: float, converged_cost_px: float)[source]
Bases:
objectA competing DT-cost minimum found away from the converged offset.
- Parameters:
offset_vu – Integer
(dv, du)shift of the competing minimum.distance_px – Euclidean distance from the converged offset.
cost_px – Mean per-vertex DT value at the competing shift.
converged_cost_px – Mean per-vertex DT value at the converged offset, measured with the same sampler so the two costs are comparable.
- build_polyline_mask(vertices_vu: NDArray[floating[Any]], shape_vu: tuple[int, int]) → NDArray[bool][source]
Render polyline vertices into a boolean image mask aligned to
shape_vu.Each vertex is rounded to the nearest integer pixel; vertices that fall outside
shape_vuare silently dropped. The integer rasterization is deliberate – the mask feedscoarse_ncc_search(), which scores integer-pixel shifts of the binary mask against the edge DT.Shared by the limb / terminator / ring-edge techniques (previously a verbatim per-module copy).
- Parameters:
vertices_vu –
(N, 2)polyline vertex positions in(v, u).shape_vu –
(H, W)target mask shape.
- Returns:
(H, W)boolean mask, True at each in-bounds rounded vertex.
- coarse_ncc_search(edge_mask: NDArray[bool], polyline_mask: NDArray[bool], search_window_vu: tuple[int, int], *, min_support_fraction: float = 0.5) → tuple[int, int][source]
Return the integer offset that maximises the normalised overlap of two masks.
For each integer shift
(dv, du)in[-margin_v, +margin_v] x [-margin_u, +margin_u]the score is the fraction of in-bounds polyline vertices that land on an edge pixel,f(dv, du) = (sum_{v, u} polyline_mask[v, u] * edge_mask[v + dv, u + du]) / N_in_bounds(dv, du), whereN_in_boundsis the number of polyline vertices that remain inside the image after the shift. Dividing the raw overlap count by the in-bounds vertex count removes the bias of a raw overlap count toward shifts that simply keep more vertices in bounds. Note this per-vertex match fraction is NOT the binary normalised cross-correlation (a binary NCC normalises bysqrt(N_in_bounds), notN_in_bounds, so its argmax can differ); the plain fraction is used because it fully cancels the in-bounds-count advantage, at the cost of over-rewarding shifts with very few surviving vertices. That failure mode is closed by the minimum-support guard: a shift whose in-bounds vertex fraction falls belowmin_support_fractionis ineligible, so a perfect score on a handful of edge-pixel stragglers cannot beat a dense well-supported match (seeDEFAULT_COARSE_MIN_SUPPORT_FRACTION).Ties are broken by the smaller
(|dv| + |du|, |dv|, dv, du)tuple, so the nearest-to-origin shift (by Manhattan distance) wins on perfectly flat inputs.- Parameters:
edge_mask –
(H, W)boolean image edge map.polyline_mask –
(H, W)boolean model polyline mask aligned toedge_mask. Must have the same shape asedge_mask.search_window_vu –
(margin_v, margin_u)non-negative integers bounding the search range in v and u.min_support_fraction – Minimum fraction of the polyline’s vertices that must remain in bounds after a candidate shift for that shift to be eligible. The zero shift keeps every mask vertex in bounds, so with any value at most 1.0 at least one candidate is always eligible when the window includes the origin.
- Returns:
(dv, du)integer offset pair at the peak.- Raises:
TypeError – if either entry of
search_window_vuis not an int.ValueError – if shapes disagree, masks are not 2-D,
search_window_vuis not a length-2 sequence of non-negative ints, ormin_support_fractionis outside[0, 1].
- coarse_ncc_search_scored(edge_mask: NDArray[bool], polyline_mask: NDArray[bool], search_window_vu: tuple[int, int], *, min_support_fraction: float = 0.5) → CoarseSearchResult[source]
Run
coarse_ncc_search()and also report the winning peak’s score.Identical search to
coarse_ncc_search()(same arguments, same validation, same tie-breaking); additionally returns the winning shift’s per-vertex match fraction so callers can gate on coarse-acquisition quality. The DT techniques call this form; the offset-only form is the convenience wrapper.- Parameters:
edge_mask – See
coarse_ncc_search().polyline_mask – See
coarse_ncc_search().search_window_vu – See
coarse_ncc_search().min_support_fraction – See
coarse_ncc_search().
- Returns:
CoarseSearchResultwith the peak offset and its match fraction.- Raises:
TypeError – if either entry of
search_window_vuis not an int.ValueError – same conditions as
coarse_ncc_search().
- find_secondary_dt_minimum(image_edge_dt: NDArray[floating[Any]], vertices_vu: NDArray[floating[Any]], *, converged_offset_vu: tuple[float, float], search_window_vu: tuple[int, int], exclude_radius_px: float, min_support_fraction: float = 0.5) → SecondaryBasin | None[source]
Search the offset window for a competing DT-cost basin.
Samples the mean per-vertex DT cost of the polyline at every integer shift in the search window and returns the best-scoring shift farther than
exclude_radius_pxfrom the converged offset. A converged LM fit whose cost is rivaled by a distant shift is not unimodal: the DT surface offers a second plausible alignment (a crater shadow, an albedo boundary, a second body’s edge), and the fit deserves no confidence in having picked the right one.- Parameters:
image_edge_dt – 2-D edge distance transform the LM fit ran against.
vertices_vu –
(N, 2)polyline vertex positions in(v, u), unshifted. A caller that fitted rotation must pass the vertices already rotated by the converged angle (the geometry the LM evaluated), or the converged cost is inflated relative to the actual fit.converged_offset_vu – The LM fit’s converged
(dv, du).search_window_vu –
(margin_v, margin_u)non-negative integers bounding the scan, normally the technique’s coarse search window.exclude_radius_px – Shifts within this Euclidean distance of the converged offset belong to the converged basin and are not candidates.
min_support_fraction – Minimum fraction of vertices that must stay in bounds for a shift to be scored.
- Returns:
The best competing basin, or None when the polyline is empty, the converged cost is unmeasurable, or no eligible shift exists.
- gradient_ridge_refine(*, vertices_vu: NDArray[floating[Any]], normals_vu: NDArray[floating[Any]], sigma_normal_per_vertex_px: NDArray[floating[Any]], gradient_magnitude: NDArray[floating[Any]], initial_offset_vu: tuple[float, float], weight_mask: NDArray[floating[Any]] | None = None, initial_rotation_rad: float = 0.0, fit_rotation: bool = False, pivot_vu: tuple[float, float] | None = None, pivot_distance_px: float = 0.0, max_iterations: int = 10, step_tolerance_px: float = 0.001, tukey_c: float = 4.685, half_width_px: float = 3.0, sample_step_px: float = 0.5, max_total_displacement_px: float = 1.5) → RidgeRefineResult[source]
Polish a polyline alignment against the continuous gradient-ridge field.
This is the final, sub-pixel stage after the coarse-NCC + DT Levenberg-Marquardt acquisition. The DT-LM minimises distance to an integer-quantized edge mask, whose zero-set snaps the recovered edge to integer pixels and leaves an SNR-independent sub-pixel-phase bias floor. This stage removes that floor by fitting directly to the continuous (un-quantized) gradient magnitude: for each vertex it finds the sub-pixel signed normal distance
t*to the gradient ridge and runs Gauss-Newton with Tukey-biweight reweighting to drive everyt*to zero.The residual for vertex
iisr_i = t*_i(signed distance from the vertex to the ridge along its outward normaln_i). Moving the vertex by a translationdeltachanges the residual by-(n_i . delta), so the translation Jacobian rows are[-n_v, -n_u]; the rotation column (when fitted) is central-differenced. The normal equations and the Tukey reweighting reuse the same machinery as the DT-LM stage.- Parameters:
vertices_vu –
(N, 2)base (unshifted) model vertices.normals_vu –
(N, 2)model normals; need not be unit length but should be (the DT techniques pass unit normals).sigma_normal_per_vertex_px –
(N,)strictly-positive prior sigma;1 / sigma**2is the prior precision weight, identical to the DT-LM stage.gradient_magnitude –
(H, W)continuous gradient magnitude image (hypotof the gradient-vector components).initial_offset_vu –
(dv, du)starting translation (the DT-LM optimum).weight_mask – optional
(N,)multiplicative mask (e.g. the DT-LM polarity acceptance) applied to every vertex weight.Nonekeeps all vertices.initial_rotation_rad – starting rotation (the DT-LM optimum).
fit_rotation – when True the parameter vector is
(dv, du, dtheta).pivot_vu – rotation pivot; defaults to the centroid of
vertices_vu.pivot_distance_px – pivot-to-image-centre distance for the rotation step-norm conversion. Required when
fit_rotationis True.max_iterations – Gauss-Newton iteration cap.
step_tolerance_px – step-norm threshold for convergence.
tukey_c – Holland-Welsch Tukey constant.
half_width_px – half-width of the normal search window.
sample_step_px – spacing of the normal sample points.
max_total_displacement_px – cap on cumulative displacement from
initial_offset_vu; exceeding it discards the refinement.
- Returns:
- Raises:
ValueError – if shape requirements are violated, the prior sigmas are not strictly positive, or
fit_rotationis True without a positivepivot_distance_px.
- information_matrix_to_covariance(jacobian: NDArray[floating[Any]], weights: NDArray[floating[Any]], *, rcond: float = 1e-09) → NDArray[floating[Any]][source]
Return the parameter covariance from a weighted Jacobian.
The M-estimator information matrix is
J^T diag(w) J; the parameter covariance is its Moore-Penrose pseudoinverse viascipy.linalg.pinvh(), which gracefully handles rank-deficient inputs (a flat polyline produces a rank-1 information matrix; the returned covariance has unbounded variance along the unobservable null direction).The
weightsargument is the per-residual weight (Tukey biweight times any prior precision) and is multiplied intoJbefore the matrix product, which both incorporates the IRLS reweighting and keeps the result symmetric within numerical tolerance.- Parameters:
jacobian –
(N, P)Jacobian of the residual vector with respect to the parameter vector.weights –
(N,)non-negative residual weights.rcond – Pseudoinverse cutoff; eigenvalues smaller than this relative to the largest are treated as null.
- Returns:
(P, P)covariance matrix. Symmetric; positive semidefinite withinrcond.- Raises:
ValueError – if shapes disagree,
weightscontains negative entries, orjacobianis not 2-D.
- lm_subpixel_refine(*, vertices_vu: NDArray[floating[Any]], normals_vu: NDArray[floating[Any]], sigma_normal_per_vertex_px: NDArray[floating[Any]], image_edge_dt: NDArray[floating[Any]], image_gradient_vu: NDArray[floating[Any]] | None = None, initial_offset_vu: tuple[float, float] = (0.0, 0.0), initial_rotation_rad: float = 0.0, fit_rotation: bool = False, pivot_vu: tuple[float, float] | None = None, pivot_distance_px: float = 0.0, use_polarity: bool = True, max_iterations: int = 30, damping: float = 0.001, step_tolerance_px: float = 0.001, tukey_c: float = 4.685, pinvh_rcond: float = 1e-09, trust_region_px: float | None = None, tikhonov_alpha: float = 0.0, final_gradient_ridge: bool = False) → LMRefineResult[source]
Refine a polyline-vs-image alignment by Levenberg-Marquardt.
The cost function is
\[C(p) = \sum_i w_i \, \bigl[\mathrm{DT}\bigl(R(\theta)\,(x_i - x_p) + x_p + (\Delta v, \Delta u)\bigr)\bigr]^2\]where \(x_i\) are the input vertices, \(x_p\) is the rotation pivot, \(R(\theta)\) is the in-plane rotation,
DTis the bilinearly-sampled image distance transform, and the per-vertex weight \(w_i\) is the product of the prior precision \(1 / \sigma_i^2\) and the Tukey biweight evaluated at the scaled residual \(r_i / \sigma_i\). The Tukey weights are recomputed after each accepted LM step (iteratively-reweighted least squares).- Parameters:
vertices_vu –
(N, 2)model vertex positions.normals_vu –
(N, 2)model outward normals (only used ifuse_polarityis True).sigma_normal_per_vertex_px –
(N,)strictly-positive prior sigma in pixels.1 / sigma**2is the prior precision weight.image_edge_dt –
(H, W)precomputed image distance transform.image_gradient_vu –
(H, W, 2)gradient vector image; required whenuse_polarityis True, ignored otherwise.initial_offset_vu –
(dv0, du0)starting translation.initial_rotation_rad – Starting rotation in radians.
fit_rotation – When True the parameter vector is
(dv, du, dtheta); otherwise(dv, du).pivot_vu –
(v_p, u_p)rotation pivot. Defaults to the centroid ofvertices_vu.pivot_distance_px – Approximate pivot-to-image-centre distance in pixels; used to convert rotation steps into pixel-equivalent increments for the convergence test. Required when
fit_rotationis True; ignored otherwise.use_polarity – When True, polarity-rejected vertices are assigned an effectively-infinite residual so the Tukey biweight zeroes their contribution.
max_iterations – Iteration cap.
damping – Initial Levenberg-Marquardt damping
lambda.step_tolerance_px – Step-norm threshold below which the iteration terminates with
converged=True.tukey_c – Holland-Welsch Tukey biweight constant.
pinvh_rcond – Pseudoinverse cutoff for the final covariance.
trust_region_px – Optional radius (pixels) around
initial_offset_vuoutside which a trial step is rejected without committing.None(default) leaves the LM unconstrained, which is the default. When set, every trial offset is checked againsthypot(trial_dv - dv0, trial_du - du0) <= trust_region_px; a violation marks the step as rejected (lambda doubled, iteration counter advanced) without updatingdv/du. This contains the joint LM + IRLS instability whereby Tukey reweighting can drag the polyline off the integer coarse-NCC seed onto an unrelated DT minimum (a crater rim, terminator edge, or surface boundary).tikhonov_alpha – Strength of a soft Tikhonov anchor pulling the translation back toward
initial_offset_vu.0(default) disables the term, which is the default. When positive, the cost adds a per-iteration penaltyalpha * sum(weights) * ||(dv, du) - (dv0, du0)||^2which scales with the data so the LM trades off raw DT improvement against displacement. The penalty is applied only to the translation degrees of freedom; rotation is never penalized. The trust region is the hard outer bound; Tikhonov pulls the LM toward the seed inside that bound when the DT cost surface has a deeper but wrong minimum on the way (crater rims, terminator edges). The anchor biases the step only; it is excluded from the reported data-only covariance (seeLMRefineResult).final_gradient_ridge – When True, after the DT Levenberg-Marquardt converges, a final
gradient_ridge_refine()stage polishes the offset against the continuous gradient-magnitude ridge, removing the sub-pixel-phase bias floor the integer-quantized DT zero-set leaves.image_gradient_vumust be supplied (it is already required whenuse_polarityis True). The reportedresiduals_px/weights/rms_px/covarianceare recomputed against the DT at the ridge-refined pose, so the spurious gates and the reported uncertainty stay on the same DT footing as without the stage.False(default) leaves the DT-LM optimum unchanged.
- Returns:
- Raises:
ValueError – if any shape requirement is violated, the prior sigmas are not strictly positive, or
fit_rotationis True without a positivepivot_distance_px.
- polarity_filter(vertices_vu: NDArray[floating[Any]], normals_vu: NDArray[floating[Any]], image_gradient_vu: NDArray[floating[Any]], *, offset_vu: tuple[float, float] = (0.0, 0.0)) → NDArray[bool][source]
Return per-vertex polarity acceptance against an image gradient.
For each vertex the helper samples the image gradient vector at the vertex’s current shifted position and compares the gradient to the model’s outward normal. The polarity test is strictly greater than zero: orthogonal hits (dot product exactly zero, vanishingly rare in floating-point arithmetic) are rejected, never silently kept.
Out-of-bounds vertices are rejected explicitly: a vertex whose rounded shifted position falls outside the image is never accepted, regardless of the gradient at the clamped boundary pixel. (Sampling the clamped pixel and “letting it reject on its own merits” is unsafe because a strong frame-edge gradient – common after zero-padding into the extended FOV – can align with an outward normal and spuriously accept an off-image vertex.) The clamp below only keeps the gather indices valid; the gathered value is discarded for such vertices.
- Parameters:
vertices_vu –
(N, 2)model vertex positions.normals_vu –
(N, 2)model outward normal at each vertex.image_gradient_vu –
(H, W, 2)per-pixel gradient image as produced byspindoctor.nav_orchestrator.image_derivatives.compute_image_gradient_vu().offset_vu –
(dv, du)shift applied to each vertex before sampling. Defaults to no shift.
- Returns:
(N,)boolean mask True where the vertex’s polarity agrees with the image’s local edge direction.- Raises:
ValueError – if shape requirements are violated.
- tukey_biweight_weights(residuals: NDArray[floating[Any]], *, c: float = 4.685) → NDArray[floating[Any]][source]
Return Tukey biweight weights for a vector of residuals.
The Holland-Welsch biweight is
\[\begin{split}w_i = \begin{cases} \bigl(1 - (r_i / c)^2\bigr)^2 & |r_i| \le c \\ 0 & \text{otherwise} \end{cases}\end{split}\]so residuals beyond
care dropped completely. Callers are responsible for scaling residuals to the desired robust scale before invoking — typically dividing by an estimate of the residual standard deviation so thatc = 4.685corresponds to the conventional 95 % asymptotic efficiency under Gaussian errors.- Parameters:
residuals –
(N,)array of (already-scaled) residuals. May contain negative entries; only the magnitude is consulted.c – Strictly positive cutoff in residual units. Must be finite.
- Returns:
(N,)float64 array of weights in[0, 1].- Raises:
ValueError – if
cis not strictly positive orresidualsis not 1-D.
BodyLimbNav — translation fit from body limb polylines.
Consumes every LIMB_ARC feature in the input set, concatenates their
per-vertex positions, weights them by 1 / sigma_normal_per_vertex_px**2,
and runs the shared distance-transform fitter to recover a single
translation that minimises the joint cost across all bodies. Multi-body
inputs improve the fit by sqrt(N_bodies) when SPICE relative geometry
is correct; the joint-translation parameterisation cannot represent
“swap two moons” mistakes by construction.
- class BodyLimbNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueBody-limb DT-based translation fit.
Consumes every
LIMB_ARCfeature whose visible arc length meets the feasibility threshold and produces one combined translation offset by minimising the summed weighted squared distance from the model polylines to the image edge distance transform. Per-vertex weights follow the prior precision1 / sigma_normal_per_vertex_px**2; Tukey biweight reweighting handles the per-image outliers.- Class attributes:
accepts_feature_types:
frozenset({LIMB_ARC}). requires_prior:False— the technique runs in pass 1.
- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.LIMB_ARC})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'at_edge', 'dt_fit_rms_px', 'lm_iterations', 'spurious', 'tukey_inlier_count', 'visible_arc_px', 'visible_limb_arc_fraction'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Return whether the input set carries any usable limb arc.
Reads only the polyline vertex count per feature — never any pixels — so the report is cheap to obtain even on large feature sets.
- Parameters:
features – Feature list filtered to this technique’s accepted types.
- Returns:
NavFeasibilityReportwithfeasible=Trueiff at least one LIMB_ARC has at least the configuredmin_arc_verticessurviving vertices.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Compute the joint-translation offset from the input limb polylines.
- Parameters:
features – Feature list filtered to the technique’s accepted types. Polylines with fewer than the configured
min_arc_verticesvertices are dropped before fitting.context – Per-image NavContext. Must carry
image_edge_dt_extandimage_gradient_vu_ext— both populated by the orchestrator’s_make_context— plusfit_camera_rotationandmax_rotation_deg.
- Returns:
A
NavTechniqueResultwith the recovered offset, calibrated confidence, and a populatedBodyLimbDiagnostics. The covariance shape and therotation_rad/sigma_rotation_radfields depend oncontext.fit_camera_rotation:False(Cassini / NHLORRI default):covariance_px2is(2, 2);rotation_radandsigma_rotation_radareNone. Any non-(2, 2)covariance returned bylm_subpixel_refine()is logged at WARNING and truncated to the 2x2 translation block.True(VGISS / GOSSI):covariance_px2is the LM M-estimator’s(3, 3)translation + rotation information matrix;rotation_radis the convergedtheta(radians) andsigma_rotation_radis the square root of the rotation diagonal. An unexpected covariance shape is treated as a programmer error and raisesRuntimeError.
BodyTerminatorNav — translation fit from body terminator polylines.
Mirrors BodyLimbNav with three terminator-specific differences:
The accepted feature type is
TERMINATOR_ARCinstead ofLIMB_ARC.Per-body uniform weighting: every vertex of a given body shares one inverse-variance weight derived from the body’s mean
sigma_normal_per_vertex_px. Cross-body weighting reflects albedo variation (low-albedo bodies provide tighter terminators than high-albedo ones).The confidence spec includes additional terms for the per-body visible-terminator-arc fraction and the phase-angle factor flag, capturing the design’s albedo / phase-geometry sensitivity.
- class BodyTerminatorNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueBody-terminator DT-based translation fit.
- Class attributes:
accepts_feature_types:
frozenset({TERMINATOR_ARC}). requires_prior:False(the technique runs in pass 1). tier:'fallback'.
The
'fallback'tier reflects that the terminator is a photometric feature modulated by phase, albedo, and local topography; its failure modes (DT-fit locking onto crater shadows or other local minima) have no per-technique signal that admits them. When a non-spurious primary fit (limb or disc) is available for the same body the ensemble drops the terminator result rather than risk letting a clean-looking but mis-converged fit override the geometric techniques.- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.TERMINATOR_ARC})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'at_edge', 'dt_fit_rms_px', 'lm_iterations', 'mean_albedo_penalty', 'mean_phase_angle_factor', 'spurious', 'tukey_inlier_count', 'visible_arc_px', 'visible_terminator_arc_fraction'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Return whether the input set carries a usable terminator arc.
Reads only the polyline vertex count per feature.
- Parameters:
features – Feature list filtered to this technique’s accepted types.
- Returns:
NavFeasibilityReportwithfeasible=Trueiff at least one TERMINATOR_ARC has at least the configuredmin_arc_verticessurviving vertices.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Compute the joint-translation offset from the input terminator polylines.
- Parameters:
features – Feature list filtered to the technique’s accepted types. Polylines with fewer than the configured
min_arc_verticesvertices are dropped before fitting.context – Per-image NavContext. Must carry
image_edge_dt_extandimage_gradient_vu_ext— both populated by the orchestrator’s_make_context— plusfit_camera_rotationandmax_rotation_deg.
- Returns:
A
NavTechniqueResultwith the recovered offset, calibrated confidence, and a populatedBodyTerminatorDiagnostics. PerBodyLimbNav.navigate:When
context.fit_camera_rotationis False the result carries a(2, 2)covariance androtation_rad/sigma_rotation_radareNone(an unexpected non-(2, 2) covariance from LM is logged at WARNING and truncated).When
context.fit_camera_rotationis True the result carries a(3, 3)covariance with the LM-fit rotation diagonal and populatedrotation_rad/sigma_rotation_rad. An unexpected covariance shape from LM raisesRuntimeError.
- requires_prior: ClassVar[bool] = False
If
True, this technique requires a prior offset onNavContextand is run only on pass 2.
- tier: ClassVar[Literal['primary', 'fallback']] = 'fallback'
Technique tier consumed by the ensemble.
'primary'techniques always vote;'fallback'techniques are dropped when any non-spurious primary result exists for the same source body (matched by body name extracted fromfeature_id). The classic example isBodyTerminatorNav(fallback) being superseded by a non-spuriousBodyLimbNavorBodyDiscCorrelateNavresult on the same body — limb / disc are geometric features whose failure modes are visibility- driven, while the terminator is a photometric feature that can mis-converge on textured surfaces with no per-technique signal to detect the failure.
BodyDiscCorrelateNav — full-disc NCC translation fit.
Consumes every BODY_DISC feature in the input set, fuses the per-body
templates into a single composite by Z-buffer paint (closer body’s pixels
overwrite farther body’s), runs the existing pyramid kpeaks NCC against
the composite, and returns one combined translation. use_gradient
defaults to 'auto' so the NCC self-selects raw vs gradient mode per
image — raw wins on smooth Lambert-shaded discs that fill the FOV;
gradient wins when only the limb carries unique-alignment signal.
Multi-body composites improve disambiguation: with N bodies the
correlation peak’s SNR grows roughly as sqrt(N) if backgrounds are
independent, and the joint geometric constraint removes the
“swap moon assignments” mode-failure that plagues per-body solo
correlation.
- class BodyDiscCorrelateNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueBody-disc full-disc NCC translation fit (multi-body, Z-buffer paint).
- Class attributes:
accepts_feature_types:
frozenset({BODY_DISC}). requires_prior:False— the technique runs in pass 1.
- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.BODY_DISC})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'at_edge', 'body_count', 'consistency_px', 'consistency_ratio', 'ncc_peak', 'peak_to_runner_up_ratio', 'spurious', 'used_gradient'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Return whether the input set carries any usable BODY_DISC feature.
Reads only feature metadata — never any pixels — so the report is cheap to obtain even on large feature sets.
- Parameters:
features – Feature list filtered to this technique’s accepted types.
- Returns:
NavFeasibilityReportwithfeasible=Trueiff at least oneBODY_DISCfeature carries a template payload.
- name: ClassVar[str] = 'BodyDiscCorrelateNav'
Human-readable technique name; used as the registry key.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Compute the joint-translation offset from the input BODY_DISC templates.
When
context.fit_camera_rotationis False the technique runs a single 2-D NCC pyramid against the unrotated composite template. When the flag is True it runs the rotation-aware schedule described in_run_3dof_pyramid()— 11 + 5 + 3 NCC pyramid evaluations across rotation samples spanning±max_rotation_deg, picking the (dv, du, theta) with the best NCC quality.- Parameters:
features – Feature list filtered to this technique’s accepted types. Features without a template payload are dropped before fitting.
context – Per-image NavContext. Reads
image_ext,sensor_mask_ext,obs.extfov_margin_vu, plusfit_camera_rotation/max_rotation_deg.
- Returns:
A
NavTechniqueResultwith the recovered offset, 2x2 or 3x3 covariance, calibrated confidence, and a populatedBodyDiscDiagnostics.
BodyBlobNav — joint-translation fit from body brightness centroids.
Consumes every BODY_BLOB feature in the input set, computes a
brightness-weighted-moment centroid for each body inside its predicted
bounding box, and recovers a single 2-D translation that maps the
predicted centroids onto the observed centroids in least-squares. With
N >= 2 blobs the fit is over-determined, which makes the technique
robust to centroid errors on any single body.
The technique reports a confidence intrinsically capped at 0.4: a
brightness-weighted centroid is much weaker than a limb fit, so even an
ideal blob match cannot dominate the ensemble. Per-blob centroid
uncertainty follows the standard CRLB scaling for a uniform-brightness
disc: sigma ~ predicted_diameter_px / (2 * sqrt(N_lit) * SNR); the
joint fit inherits that scaling.
- class BodyBlobNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueBody-blob brightness-weighted centroid translation fit.
- Class attributes:
accepts_feature_types:
frozenset({BODY_BLOB}). requires_prior:False(the technique runs in pass 1). tier:'fallback'.
The
'fallback'tier reflects that the brightness-weighted centroid is a weaker observation than a limb fit (already reflected in the technique’shard_cap: 0.4). When a non-spurious primary fit (limb or disc) is available for the same body the ensemble drops the blob result rather than dilute the geometric techniques’ answer with the centroid’s lit-hemisphere bias.- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.BODY_BLOB})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'at_edge', 'blob_count', 'body_extent_px', 'body_snr_inside_predicted_bbox', 'max_phase_angle_deg', 'max_phase_irregularity_factor', 'residual_px'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Return whether the input set carries any usable BODY_BLOB feature.
Reads only feature metadata; never any pixels. The technique requires at least one
BODY_BLOBwith a non-zero predicted diameter — otherwise the centroid moment is degenerate.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Compute the joint translation that maps predicted to observed centroids.
- Parameters:
features – Feature list filtered to the technique’s accepted types. Blobs that fall outside the extfov or have no above-noise signal in their predicted bbox are dropped.
context – Per-image NavContext. Reads
image_ext,image_noise_sigma,obs.extfov_margin_vu, andfit_camera_rotation.
- Returns:
A
NavTechniqueResultwith the recovered offset, calibrated confidence, and a populatedBodyBlobDiagnostics. The covariance shape and the rotation fields depend oncontext.fit_camera_rotation:False(the default Cassini / NHLORRI posture):covariance_px2is(2, 2)androtation_rad/sigma_rotation_radareNone.True(VGISS / GOSSI):covariance_px2is the rank-deficient(3, 3)form returned byembed_rotation_unobservable()(a brightness-weighted centroid is rotation-invariant about itself, so the technique carries no rotation evidence);rotation_radis0.0andsigma_rotation_radis the unobservable sentinel.
- requires_prior: ClassVar[bool] = False
If
True, this technique requires a prior offset onNavContextand is run only on pass 2.
- tier: ClassVar[Literal['primary', 'fallback']] = 'fallback'
Technique tier consumed by the ensemble.
'primary'techniques always vote;'fallback'techniques are dropped when any non-spurious primary result exists for the same source body (matched by body name extracted fromfeature_id). The classic example isBodyTerminatorNav(fallback) being superseded by a non-spuriousBodyLimbNavorBodyDiscCorrelateNavresult on the same body — limb / disc are geometric features whose failure modes are visibility- driven, while the terminator is a photometric feature that can mis-converge on textured surfaces with no per-technique signal to detect the failure.
RingEdgeNav — translation fit from ring-edge polylines.
Consumes every RING_EDGE feature in the input set and produces a
single combined translation by minimising the joint distance-transform
cost. When every input ring edge is flagged is_straight_line the
combined Jacobian is rank-deficient — all parallel ring edges share a
single ring-plane normal, so the along-edge axis is unobservable. The
returned covariance is honestly rank-1 in that case; the ensemble
combine fuses it with any orthogonal-axis result (a star, body limb,
body blob) before declaring a final answer.
- class RingEdgeNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueRing-edge DT-based translation fit.
- Class attributes:
accepts_feature_types:
frozenset({RING_EDGE}). requires_prior:False— the technique runs in pass 1.
- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.RING_EDGE})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'at_edge', 'edge_count', 'is_rank_1', 'per_edge_dt_median_max', 'per_edge_dt_rms_mean', 'per_edge_dt_rms_summed', 'total_edge_length_px'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Return whether the input set carries any usable ring edge.
Reads only the polyline vertex count per feature. A single non-empty ring-edge polyline is sufficient: even an all-flat scene produces a useful rank-1 constraint that the ensemble will fuse with another feature.
- Parameters:
features – Feature list filtered to this technique’s accepted types.
- Returns:
NavFeasibilityReportwithfeasible=Trueiff at least one RING_EDGE has a non-empty polyline.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Compute the joint-translation offset from ring-edge polylines.
- Parameters:
features – Feature list filtered to the technique’s accepted types.
context – Per-image NavContext. Must carry
image_edge_dt_extandimage_gradient_vu_ext— both populated by the orchestrator’s_make_context.
- Returns:
A
NavTechniqueResultwith the recovered offset, 2x2 covariance (rank-1 when every input edge is straight), calibrated confidence, and a populatedRingEdgeDiagnostics.
- aggregate_edge_normal_angle_deg(features: list[NavFeature]) → float | None[source]
Return the dominant edge-normal orientation of an all-straight scene.
The angle is in degrees in the
(v, u)frame, measured from+vtoward+u(the unit normal is(cos, sin)) — the same convention as the sidecar schema’sground_truth.constraint.normal_angle_deg. The orientation is the dominant eigenvector of the per-vertex normals’ outer-product sum, so it is independent of each edge’s polarity sign.- Parameters:
features – Any feature list; only
RING_EDGEpolylines are read.- Returns:
The orientation in degrees, normalised to
(-90, 90], orNoneunless at least one ring-edge polyline is present and every one is straight — the constraint direction is only meaningful for a rank-1 scene.
RingAnnulusNav — multi-ring composite NCC translation fit.
Consumes every RING_ANNULUS feature in the input set, fuses the per-
planet templates into a single composite by Z-buffer paint (closer ring
system’s pixels overwrite farther ones), runs the existing pyramid
kpeaks NCC against the composite, and returns one combined translation.
use_gradient defaults to 'auto' so the NCC self-selects raw vs
gradient mode per image — raw wins on broad-brightness-gradient ring
geometries (low-resolution Saturn rings where the C-ring is uniformly
dim), gradient wins when sharp ringlet edges dominate.
Multi-planet annulus composites improve disambiguation in the same way
as multi-body disc composites: each annulus contributes its own
translational constraint to the joint NCC peak, the geometric alignment
between ring systems removes the “swap planet assignments” ambiguity,
and the SNR of the combined peak grows roughly as sqrt(N) if
backgrounds are independent. Multi-planet scenes are rare but real
(the Cassini approach phase imaged Jupiter and Saturn together; New
Horizons imaged Jupiter from Pluto distance), so is_feasible must
handle len(features) > 1.
- class RingAnnulusNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueRing-annulus full-template NCC translation fit (multi-planet, Z-buffer paint).
- Class attributes:
accepts_feature_types:
frozenset({RING_ANNULUS}). requires_prior:False— the technique runs in pass 1.
- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.RING_ANNULUS})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'annulus_count', 'at_edge', 'ncc_peak', 'peak_to_runner_up_ratio', 'spurious', 'used_gradient'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Return whether the input set carries any usable RING_ANNULUS feature.
Reads only feature metadata — never any pixels — so the report is cheap to obtain even on large feature sets. The feature list may carry one
RING_ANNULUSper detectable ring system in multi-planet scenes; the technique handleslen(features) > 1by Z-buffer painting all annuli into one combined template.- Parameters:
features – Feature list filtered to this technique’s accepted types.
- Returns:
NavFeasibilityReportwithfeasible=Trueiff at least oneRING_ANNULUSfeature carries a template payload.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Compute the joint-translation offset from the input RING_ANNULUS templates.
- Parameters:
features – Feature list filtered to this technique’s accepted types. Features without a template payload are dropped before fitting.
context – Per-image NavContext. Reads
image_ext,sensor_mask_ext,obs.extfov_margin_vu, andfit_camera_rotation.
- Returns:
A
NavTechniqueResultwith the recovered offset, calibrated confidence, and a populatedRingAnnulusDiagnostics. The covariance shape and the rotation fields depend oncontext.fit_camera_rotation:False(the default Cassini / NHLORRI posture):covariance_px2is(2, 2)androtation_rad/sigma_rotation_radareNone.True: the translation NCC pyramid carries no rotation evidence, so the result reports the rank-deficient(3, 3)form returned byembed_rotation_unobservable()withrotation_rad = 0.0andsigma_rotation_radequal to the rotation-unobservable sentinel. Multi- planet ring scenes that warrant a 3-D NCC pyramid are tracked for Phase 12+; the rank-deficient encoding flows through the ensemble combine without contaminating other techniques’ rotation slots.
StarFieldFromCatalogNav — multi-star RANSAC pattern matcher.
The technique is the long-pole prior-free star fit: it makes no assumption about which detection corresponds to which catalog star, and recovers a 2-D translation purely from the geometry of bright sources in the image matched against the predicted catalog field.
Algorithm — four sub-pieces:
Source detection. The image is matched-filtered against a small Gaussian PSF kernel; pixels that are local maxima above
detection_sigma * image_noise_sigmaare accepted; a brightness- weighted centroid pins each peak’s sub-pixel position. The brightestmax_sourcessurvivors (default 30) feed the matcher.Triplet hashing. For every unordered triplet of detected sources
{A, B, C}withAbrightest, the hash(d_AB / d_AC, d_BC / d_AC, ∠BAC)is computed. The same hash is computed for catalog triplets (A= brightest by predicted SNR). The hash is similarity-invariant — translation, rotation, and uniform scale all leave it unchanged — so the matcher recovers correspondences without already knowing the offset.RANSAC. Each (detection-triplet, catalog-triplet) candidate is scored by counting detection-to-catalog inliers under the translation it implies. Candidates are iterated in deterministic order — sorted first by hash distance ascending, then by sorted
(idx_A, idx_B, idx_C)ascending — so the matcher’s choice of winner is fully reproducible (Cardinal Principle 3, Part 3 §”Determinism in RANSAC”).Verification. With the best transform’s inlier set, the technique refits the translation by Tukey-biweight-reweighted least squares; the per-axis variance of the surviving residuals becomes the reported covariance.
Phase 8 ships translation-only fitting; the
fit_camera_rotation rotation upgrade arrives in Phase 9.
- class StarFieldFromCatalogNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueMulti-star pattern-match translation fit (≥ 3 catalog + image stars).
- Class attributes:
accepts_feature_types:
frozenset({STAR}). requires_prior:False— runs in pass 1.
- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.STAR})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'at_edge', 'median_residual_px', 'n_catalog_predicted', 'n_detected_sources', 'n_inliers', 'spurious'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Report feasibility based on the predictable-star cohort.
Reads only feature metadata; never any pixels. Feasible when at least three usable STAR features are in the input set — below that the matcher cannot form a single triplet.
- name: ClassVar[str] = 'StarFieldFromCatalogNav'
Human-readable technique name; used as the registry key.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Recover translation from triplet pattern matching against the catalog.
- Parameters:
features – STAR features (the orchestrator pre-filters by type). At least three must survive
usable_stars.context – Per-image NavContext.
- Returns:
NavTechniqueResultwith the recovered offset, 2x2 covariance, calibrated confidence, and a populatedStarFieldDiagnostics.
StarRefineNav — pass-2 star-refinement technique.
Consumes the pass-1 ensemble’s prior offset and refines it via local PSF-centroid fits on every predicted catalog star. For each STAR feature, the technique:
Shifts the catalog prediction by the prior offset.
Looks for a brightness peak inside a small refinement window centered on that shifted prediction.
Fits a brightness-weighted moment around the peak to get a sub-pixel centroid.
Computes per-star residuals (observed - shifted_prediction) and averages them in inverse-variance fashion.
Per-star inverse variance is the trace of the feature’s CRLB
covariance carried on NavFeature.position_cov_px; stars with low
predicted SNR get less weight. Stars whose detection is too far from
the shifted prediction (likely a wrong peak) are dropped before the
fit.
The refined offset is reported as a delta from the prior — the ensemble combine adds it back. The covariance reflects the residual scatter across the surviving stars; with two or more inliers the technique reports the actual scatter, with a single inlier the CRLB floor.
- class StarRefineNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueStar-refinement technique consuming the pass-1 prior.
A 1-inlier refine carries no independent cross-check information beyond the prior it was handed — it is the same single observation that drove the pass-1 fit, just polished. The post-sigmoid
single_inlier_confidence_cap(default 0.5) prevents the technique from promoting itself aboveStarUniqueMatchNav’s 0.7 1-star cap on the same observation. With ≥ 2 inliers the per- star residual scatter cross-checks the joint fit and the cap is not applied.- Class attributes:
accepts_feature_types:
frozenset({STAR}). requires_prior:True— runs in pass 2.
- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.STAR})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'at_edge', 'median_pos_err_px', 'n_stars_used', 'residual_scatter_px', 'spurious'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Report feasibility based on the predictable-star cohort.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Refine the pass-1 prior by per-star centroid residuals.
- Parameters:
features – STAR features (the orchestrator pre-filters by type).
context – Per-image NavContext. Must carry
prior_offset_px; the technique is registered asrequires_prior=Trueso the orchestrator only invokes it on pass 2.
- Returns:
NavTechniqueResultwith the delta offset relative to the prior, a 2x2 covariance, calibrated confidence, and a populatedStarRefineDiagnostics.
StarUniqueMatchNav — translation fit when 1 or 2 stars are uniquely matched.
Two paths share one technique:
One-star path. When the catalog reduction produces one star whose predicted brightness is at least
brightness_margin_to_next_catalog_star_magbrighter than the next-brightest predictable star, the brightest detection inside its search window is unambiguously its match. The offset is the centroid minus the prediction; confidence is capped at the configured one-star limit (default 0.7) because a single match cannot cross-check itself.No-rival sentinels: the
one_star_min_peak_ratioambiguity gate and the brightness-margin gate both reportinfwhen no rival exists (no runner-up detection above the window background / no other predictable catalog star). The sentinels deliberately PASS the ratio checks – a genuinely unique bright star has no rival and must remain matchable – but an infinite peak ratio also means the ambiguity gate measured nothing (a flat or quantized window, exactly where a lone hot pixel or artifact would otherwise auto-pass every check). In that vacuous case acceptance is additionally gated on the prediction-to-detection distance staying withinone_star_max_residual_px: with no rival statistics to lean on, a lone detection is only promoted to an identification when it sits inside the pointing-prior core. A finite peak ratio leaves acceptance to the measured ambiguity gate; genuine one-star matches with offsets up to ~24 px exist in the operator-verified library, so no uniform residual cut below the search window is possible.Two-star path. With two predictable stars, the technique tries both detection-to-prediction assignments and picks the one whose joint residual is smaller. The residual cross-checks the assignment; confidence is capped at the configured two-star limit (default 0.8).
Local-window detection is intentional: the search window is sized to the per-instrument SPICE pointing-error envelope, so a brightest peak inside the window is the matched detection. No global star-detection pass is needed (and this technique is feasible even on images where multi-star detection would fail because the rest of the field is too faint or too crowded).
- class StarUniqueMatchNav(*, config: Config | None = None)[source]
Bases:
NavTechniqueStar unique-match translation fit (1- or 2-star path).
- Class attributes:
accepts_feature_types:
frozenset({STAR}). requires_prior:False— runs in pass 1.
- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.STAR})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- confidence_attributes: ClassVar[frozenset[str]] = frozenset({'at_edge', 'brightness_margin_mag', 'predicted_snr', 'residual_px', 'spurious'})
Names of every attribute the technique’s confidence spec may read (diagnostics fields plus side-channel flags such as
at_edge).validate_registered_confidence_specsensures every term inconfidence_specreferences a member of this set.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Report feasibility based on the predictable-star cohort.
Reads only feature metadata; never any pixels. Feasible when at least one usable STAR feature is in the input set; the navigate path then decides whether the 1-star or 2-star branch wins.
- name: ClassVar[str] = 'StarUniqueMatchNav'
Human-readable technique name; used as the registry key.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Recover translation from the 1- or 2-star uniquely-bright match.
- Parameters:
features – STAR features (the orchestrator pre-filters by type).
context – Per-image NavContext.
- Returns:
A
NavTechniqueResultwith the recovered offset, calibrated confidence (capped per-mode byone_star_confidence_cap/two_star_confidence_cap), and a populatedStarUniqueMatchDiagnostics. The covariance shape and rotation fields depend on the chosen path and oncontext.fit_camera_rotation:1-star path, fit_camera_rotation=False:
(2, 2)covariance;rotation_radandsigma_rotation_radareNone.1-star path, fit_camera_rotation=True: rank-deficient
(3, 3)covariance viaembed_rotation_unobservable()(a single match cannot constrain rotation);rotation_rad = 0.0andsigma_rotation_radis the rotation-unobservable sentinel.2-star path, fit_camera_rotation=False:
(2, 2)covariance from the per-feature CRLB floor; rotation fieldsNone.2-star path, fit_camera_rotation=True: full
(3, 3)covariance with the analytic2 * (sigma_v**2 + sigma_u**2) / L**2rotation diagonal (Lis the catalog-pair separation);rotation_radis the Procrustes-fit angle andsigma_rotation_radis the square root of the rotation diagonal.
Manual navigation technique — interactive PyQt6 dialog.
Renders the composite predicted scene from every NavModel’s template
features and lets the operator pick a (dv, du) offset by hand. An
Auto button inside the dialog runs the same masked-NCC pyramid that
correlation-based techniques use, so the operator can either accept the
auto-pick or override it manually.
This technique is not part of the autonomous pipeline; it does not appear
in the NavTechnique._registry and is invoked directly by an
interactive driver. The plan calls for replacing the dialog’s single
Auto button with a per-technique side-by-side panel showing each
registered technique’s proposal; that redesign is deferred and tracked
separately.
- class NavTechniqueManual(*, config: Config | None = None, annotations: Annotations | None = None)[source]
Bases:
NavTechniqueInteractive manual navigation.
Composes every renderable feature into a single ext-FOV image plus mask via
compose_dialog_overlay(), hands the result plus the observation to theManualNavDialog, and packages the operator’s choice into aNavTechniqueResult. Renderable feature kinds are template-bearing (BODY_DISC,RING_ANNULUS,CARTOGRAPHIC_MODEL), polyline-bearing (LIMB_ARC,TERMINATOR_ARC,RING_EDGE),BODY_BLOB(predicted-diameter circle outline), andSTAR(rectangle outline at the predicted-vu position sized by the per-feature PSF bbox).- Class attributes:
- _abstract:
True— kept out of the auto-discovery registry so the orchestrator does not invoke the dialog during background navigation runs.
- accepts_feature_types: every feature type — manual navigation
looks at whatever the scene has rendered.
- _abstract:
- accepts_feature_types: ClassVar[frozenset[NavFeatureType]] = frozenset({NavFeatureType.BODY_BLOB, NavFeatureType.BODY_DISC, NavFeatureType.CARTOGRAPHIC_MODEL, NavFeatureType.LIMB_ARC, NavFeatureType.RING_ANNULUS, NavFeatureType.RING_EDGE, NavFeatureType.STAR, NavFeatureType.TERMINATOR_ARC, NavFeatureType.TITAN_LIMB})
Frozen set of feature types this technique consumes. The orchestrator skips invocation when no input feature has a matching type.
- is_feasible(features: list[NavFeature]) → NavFeasibilityReport[source]
Manual navigation runs whenever there is anything to render.
Four feature kinds paint into the dialog’s composite overlay (see
compose_dialog_overlay()):template-bearing (
BODY_DISC,RING_ANNULUS,CARTOGRAPHIC_MODEL) — full bitmap silhouette;polyline-bearing (
LIMB_ARC,TERMINATOR_ARC,RING_EDGE) — single-pixel marks at every vertex;BODY_BLOB— 1-pixel circle outline at the predicted centroid with the predicted-diameter radius;STAR— rectangle outline at the predicted-vu position sized by the per-feature PSF bbox so the operator can see where the catalog says the star sits.
Without any of them the dialog has nothing to display.
- name: ClassVar[str] = 'NavTechniqueManual'
Human-readable technique name; used as the registry key.
- navigate(features: list[NavFeature], context: NavContext) → NavTechniqueResult[source]
Run the dialog and convert the operator’s choice to a result.
Cancelling the dialog yields a spurious result with zero confidence so the ensemble drops it; accepting the dialog yields a result with
_MANUAL_OFFSET_SIGMA_PXper-axis covariance.
- run_manual_nav(obs: ObsSnapshotInst, *, config: Config | None = None) → NavResult | None[source]
Open the manual-navigation dialog on a single observation.
Builds the same NavContext + features the autonomous orchestrator would see, then opens
ManualNavDialogso the operator can pick an offset by hand. When the operator accepts a pick, the dialog’s offset is wrapped in a fullNavResult(provenance + image classifier + feature inventory + annotations populated identically to the autonomous pipeline), so callers can write the same_metadata.jsonand_summary.pngoutputsnavigate_image_filesproduces.- Parameters:
obs – Loaded observation snapshot to navigate.
config – Optional
Configoverride; defaults toDEFAULT_CONFIG.
- Returns:
A
NavResultwithstatus='success'on accept, orNonewhen the operator cancels or no supported overlay feature paints any pixels into the ext-FOV composite. Supported overlay types matchNavTechniqueManual.is_feasible(): template-bearing features (BODY_DISC/RING_ANNULUS/CARTOGRAPHIC_MODEL), polyline-bearing features (LIMB_ARC/TERMINATOR_ARC/RING_EDGE),BODY_BLOB(1-pixel circle outline at the predicted centroid), andSTAR(rectangle outline at the predicted-vu position sized by the per-feature PSF bbox). The dialog is opened only when the composed mask is non-empty; an off-frame blob, an off-image star, or a polyline whose vertices all clip out-of-bounds is treated as if no renderable feature were present.