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: object

Model-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_scale multiplies the inter-pyramid-level peak migration (applied inside the correlator, carried here for completeness of the covariance model).

  • model_error_size_frac scales 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_px is a size-independent absolute floor (pointing, camera distortion, sub-pixel interpolation).

Parameters:
  • localization_uncertainty_scale – Multiplier on the correlator consistency (px); 0.0 disables it.

  • model_error_size_frac – Fraction of the template extent (px) taken as the silhouette-error sigma; 0.0 disables it.

  • model_error_floor_px – Absolute floor (px); 0.0 disables it.

localization_uncertainty_scale: float
model_error_floor_px: float
model_error_size_frac: float
class NavTechnique(*, config: Config | None = None, **kwargs: Any)[source]

Bases: NavBase, ABC

Abstract base for autonomous-navigation techniques.

Concrete subclasses self-register via __init_subclass__. The orchestrator iterates NavTechnique._registry to discover the techniques it should run. Subclasses must override the class attributes name, accepts_feature_types, and (when relevant) requires_prior.

classmethod __init_subclass__(**kwargs: Any) None[source]

Auto-register concrete subclasses.

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_specs ensures every term in confidence_spec references a member of this set.

confidence_spec: ClassVar[ConfidenceSpec | None] = None

Confidence-formula spec consumed by evaluate_sigmoid_combination. Loaded from config_510_techniques.yaml and assigned at Config.read_config time. None for 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_feasible reads feature metadata only — never pixels. It is called before navigate and 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 NavFeasibilityReport whose feasible field tells the orchestrator whether to invoke navigate.

name: ClassVar[str] = ''

Human-readable technique name; used as the registry key.

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 NavTechniqueResult with offset, covariance, confidence, and per-technique diagnostics.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and 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 from feature_id). The classic example is BodyTerminatorNav (fallback) being superseded by a non-spurious BodyLimbNav or BodyDiscCorrelateNav result 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_deg at which the rotation parameter trips the technique’s at_edge flag.

When the converged |theta| exceeds rotation_at_edge_fraction * max_rotation_deg the 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.yaml under techniques.<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 +inf would break covariance validation (np.linalg.eigvalsh returns 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 covariance with floor_px**2 added 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.0 returns 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**2 to 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_2x2 is not a 2x2 ndarray.

filter_technique_names(names: list[str], patterns: str | list[str]) list[str][source]

Return names filtered 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_px tunable.

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 tuning mapping.

  • 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 tuning mapping.

  • 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_threshold the 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 ConfidenceBreakdown returned by evaluate_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_vu to the image centre, floored at 1.0 so 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_rad on their NavTechniqueResult when 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_vu to 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 an AttributeError rather than silently falling through to a fabricated default.

technique_tier(technique_name: str) str[source]

Look up a registered technique’s tier from the registry.

Returns 'primary' for an unregistered name so unknown techniques (test stubs, future plug-ins) are treated as primary by construction; an explicit tier='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 by spindoctor.config.config.Config._validate_registered_techniques()) must declare the full set of valid attribute names in confidence_attributes. Every term’s feature and every hard_zero_if key 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 is None (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: object

Outcome 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 feasible is 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_count is negative or feasible=False is supplied with an empty reason.

__post_init__() None[source]

Validate types and invariants on every field.

consumed_feature_count: int = 0
feasible: bool
reason: str

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: object

One 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 a list is 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); None on 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 parsing feature_ids strings.

  • 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.

__post_init__() None[source]

Validate covariance shape and freeze the array.

at_edge: bool
confidence: float
covariance_px2: NDArray[floating[Any]]
diagnostics: BodyDiscDiagnostics | BodyLimbDiagnostics | BodyTerminatorDiagnostics | BodyBlobDiagnostics | ManualNavDiagnostics | RingEdgeDiagnostics | RingAnnulusDiagnostics | StarFieldDiagnostics | StarUniqueMatchDiagnostics | StarRefineDiagnostics
feature_ids: tuple[str, ...]
offset_px: tuple[float, float]
prior_source_techniques: frozenset[str] = frozenset({})
rotation_rad: float | None = None
sigma_rotation_rad: float | None = None
source_bodies: frozenset[str] = frozenset({})
spurious: bool
technique_name: str

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: object

Diagnostics 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_factor instead 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'}
blob_count: int = 0
body_extent_px: float = 0.0
body_snr_inside_predicted_bbox: float = 0.0
max_phase_angle_deg: float = 0.0
max_phase_irregularity_factor: float = 0.0
residual_px: float = 0.0
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: object

Diagnostics 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_ratioconsistency_px divided 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.

CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'body_count': 'body_count', 'consistency_px': 'consistency_px', 'consistency_ratio': 'consistency_ratio', 'ncc_peak': 'ncc_peak', 'peak_to_runner_up_ratio': 'peak_to_runner_up_ratio', 'used_gradient': 'used_gradient'}
body_count: int = 0
consistency_px: float = 0.0
consistency_ratio: float = 0.0
ncc_peak: float = 0.0
peak_to_runner_up_ratio: float = 0.0
used_gradient: bool = False
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: object

Diagnostics 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'}
coarse_peak_fraction: float = 0.0
dt_fit_rms_px: float = 0.0
lm_converged: bool = True
lm_iterations: int = 0
polarity_rejection_fraction: float = 0.0
tukey_inlier_count: int = 0
visible_arc_px: float = 0.0
visible_limb_arc_fraction: float = 0.0
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: object

Diagnostics emitted by BodyTerminatorNav.

Parameters: same shape as BodyLimbDiagnostics with visible_terminator_arc_fraction substituted, 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; None when the scan did not run or found no eligible shift (0.0 is 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_threshold mark the fit spurious. None when 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'}
coarse_peak_fraction: float = 0.0
dt_fit_rms_px: float = 0.0
lm_converged: bool = True
lm_iterations: int = 0
mean_albedo_penalty: float = 0.0
mean_phase_angle_factor: float = 0.0
polarity_rejection_fraction: float = 0.0
secondary_basin_cost_ratio: float | None = None
secondary_basin_distance_px: float | None = None
tukey_inlier_count: int = 0
visible_arc_px: float = 0.0
visible_terminator_arc_fraction: float = 0.0
class ManualNavDiagnostics(operator_accepted: bool = True)[source]

Bases: object

Diagnostics emitted by NavTechniqueManual.

Parameters:

operator_acceptedTrue when the operator confirmed the dialog’s chosen offset. Always True on results that reach the curator (cancelled picks short-circuit before the NavResult is built), but kept explicit so the JSON metadata records the fact that a human, not an autonomous technique, set the offset.

CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'operator_accepted': 'operator_accepted'}
operator_accepted: bool = True
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: object

Diagnostics 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.

CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'annulus_count': 'annulus_count', 'ncc_peak': 'ncc_peak', 'peak_to_runner_up_ratio': 'peak_to_runner_up_ratio', 'used_gradient': 'used_gradient'}
annulus_count: int = 0
ncc_peak: float = 0.0
peak_to_runner_up_ratio: float = 0.0
used_gradient: bool = False
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: object

Diagnostics 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.0 when 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'}
coarse_peak_fraction: float = 0.0
edge_count: int = 0
is_rank_1: bool = False
lm_converged: bool = True
per_edge_dt_median_max: float = 0.0
per_edge_dt_rms_mean: float = 0.0
per_edge_dt_rms_summed: float = 0.0
sigma_orbit_radial_px: float = 0.0
total_edge_length_px: float = 0.0
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: object

Diagnostics 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'}
median_residual_px: float = 0.0
n_catalog_predicted: int = 0
n_detected_sources: int = 0
n_inliers: int = 0
n_triplets_evaluated: int = 0
rotation_below_separability_floor: bool = False
class StarRefineDiagnostics(n_stars_used: int = 0, median_pos_err_px: float = 0.0, residual_scatter_px: float = 0.0)[source]

Bases: object

Diagnostics 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.

CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'median_pos_err_px': 'median_pos_err_px', 'n_stars_used': 'n_stars_used', 'residual_scatter_px': 'residual_scatter_px'}
median_pos_err_px: float = 0.0
n_stars_used: int = 0
residual_scatter_px: float = 0.0
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: object

Diagnostics 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; +inf when 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 (inf when the runner-up does not clear the background; 0.0 on the 2-star path, which cross-checks via its assignment residual instead).

CURATOR_FIELDS: ClassVar[dict[str, str | None]] = {'brightness_margin_mag': 'brightness_margin_mag', 'detection_peak_ratio': 'detection_peak_ratio', 'mode': 'mode', 'predicted_snr': 'predicted_snr', 'residual_px': 'residual_px'}
brightness_margin_mag: float = 0.0
detection_peak_ratio: float = 0.0
mode: str = ''
predicted_snr: float = 0.0
residual_px: float = 0.0

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: object

Per-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_if attribute that fired, or None when no hard-zero gate applied.

  • hard_cap_applied – True if the final sigmoid value was clamped by spec.hard_cap.

alpha0: float
confidence: float
hard_cap_applied: bool
hard_zero: str | None
sigmoid_arg: float
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: object

Full confidence formula spec for one NavTechnique.

Parameters:
  • alpha0 – Constant term in the sigmoid argument.

  • terms – Tuple of ConfidenceTerm linear 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 alpha0 is not numeric, terms is not a tuple of ConfidenceTerm, or hard_zero_if is not a mapping of str to bool.

  • ValueError – if hard_cap is outside [0, 1].

__post_init__() None[source]

Validate types and ranges of every field.

alpha0: float
hard_cap: float | None = None
hard_zero_if: dict[str, bool]
terms: tuple[ConfidenceTerm, ...] = ()
class ConfidenceTerm(feature: str, alpha: float, offset: float = 0.0, divisor: float = 1.0, cap_at: float | None = None)[source]

Bases: object

One 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, or cap_at is not numeric.

  • ValueError – if divisor is zero or cap_at is outside [0, 1].

__post_init__() None[source]

Validate numeric types and divisor / cap_at ranges.

alpha: float
cap_at: float | None = None
divisor: float = 1.0
feature: str
offset: float = 0.0
class ConfidenceTermContribution(feature: str, raw: float, normalized: float, alpha: float, contribution: float)[source]

Bases: object

One term’s contribution to the sigmoid argument.

Parameters:
  • feature – The diagnostic-attribute name.

  • raw – The raw value read off the diagnostics object.

  • normalizedraw after offset / divisor / cap_at.

  • alpha – Linear coefficient applied.

  • contributionalpha * normalized — the term’s signed contribution to the sigmoid argument.

alpha: float
contribution: float
feature: str
normalized: float
raw: float
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) when return_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: ValueError

Raised when config_510_techniques.yaml cannot 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 ConfidenceSpec for technique_name.

Reads the techniques[technique_name] mapping and constructs a ConfidenceSpec from its fields. All numeric coercion and range checks happen here so per-technique modules stay free of YAML-shape bookkeeping. Callers typically pass Config.category('techniques') (an AttrDict, which subclasses dict).

Parameters:
  • techniques – Mapping of NavTechnique.name to its raw config block (the techniques section of config_510_techniques.yaml).

  • technique_nameNavTechnique.name to look up.

Returns:

A frozen ConfidenceSpec ready to assign to the technique’s confidence_spec class 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 tuning block for technique_name.

Returns the dict-shaped tuning sub-block as a flat {key: number} mapping with all numeric values coerced to float or int (booleans rejected — YAML’s loose numeric types make True look 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.name to its raw config block (the techniques section of config_510_techniques.yaml).

  • technique_nameNavTechnique.name to look up.

Returns:

{key: value} or an empty mapping when the technique block has no tuning sub-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:

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: object

Structured 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.0 when 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.

offset_vu: tuple[int, int]
score: float
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: object

Structured output of lm_subpixel_refine().

Parameters:
  • offset_vu(dv, du) converged translation in pixels.

  • rotation_rad – Converged rotation in radians; 0.0 when fit_rotation was False.

  • covariance(2, 2) or (3, 3) parameter covariance derived from the M-estimator information matrix J^T diag(w) J at the converged point. This is the DATA-ONLY covariance: it deliberately EXCLUDES the Tikhonov anchor diagonal that tikhonov_alpha adds 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 > floor test 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 weighted rms_px collapses to near zero on exactly such a mis-convergence and slips past a rms_px > floor gate; the raw RMS retains those Tukey outliers and surfaces the bad fit. Polarity-rejected vertices are excluded (they carry the _INFINITY_DT_PENALTY_PX sentinel 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. False means 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 == 0 or the surviving weights sum to zero). In this case rms_px is +inf and covariance is all-inf; consumers treat it as a spurious fit.

  • polarity_rejected_count – Number of vertices the polarity filter rejected at the seed offset (0 when use_polarity was False). polarity_rejected_count / N is the polarity-rejection fraction the fit-quality gates consume.

__post_init__() None[source]

Freeze the numpy arrays and validate shapes.

converged: bool
covariance: NDArray[floating[Any]]
degenerate: bool
inlier_count: int
iterations: int
offset_vu: tuple[float, float]
polarity_rejected_count: int = 0
raw_rms_px: float
residuals_px: NDArray[floating[Any]]
rms_px: float
rotation_rad: float
weights: NDArray[floating[Any]]
class RidgeRefineResult(offset_vu: tuple[float, float], rotation_rad: float, iterations: int, converged: bool, applied: bool)[source]

Bases: object

Structured 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_rotation was 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_rad equal the inputs unchanged and the caller keeps the DT-LM pose.

applied: bool
converged: bool
iterations: int
offset_vu: tuple[float, float]
rotation_rad: float
class SecondaryBasin(offset_vu: tuple[int, int], distance_px: float, cost_px: float, converged_cost_px: float)[source]

Bases: object

A 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.

converged_cost_px: float
cost_px: float
distance_px: float
offset_vu: tuple[int, int]
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_vu are silently dropped. The integer rasterization is deliberate – the mask feeds coarse_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.

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), where N_in_bounds is 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 by sqrt(N_in_bounds), not N_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 below min_support_fraction is ineligible, so a perfect score on a handful of edge-pixel stragglers cannot beat a dense well-supported match (see DEFAULT_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 to edge_mask. Must have the same shape as edge_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_vu is not an int.

  • ValueError – if shapes disagree, masks are not 2-D, search_window_vu is not a length-2 sequence of non-negative ints, or min_support_fraction is 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:
Returns:

CoarseSearchResult with the peak offset and its match fraction.

Raises:
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_px from 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 every t* to zero.

The residual for vertex i is r_i = t*_i (signed distance from the vertex to the ridge along its outward normal n_i). Moving the vertex by a translation delta changes 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**2 is the prior precision weight, identical to the DT-LM stage.

  • gradient_magnitude(H, W) continuous gradient magnitude image (hypot of 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. None keeps 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_rotation is 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:

RidgeRefineResult.

Raises:

ValueError – if shape requirements are violated, the prior sigmas are not strictly positive, or fit_rotation is True without a positive pivot_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 via scipy.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 weights argument is the per-residual weight (Tukey biweight times any prior precision) and is multiplied into J before 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 within rcond.

Raises:

ValueError – if shapes disagree, weights contains negative entries, or jacobian is 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, DT is 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 if use_polarity is True).

  • sigma_normal_per_vertex_px(N,) strictly-positive prior sigma in pixels. 1 / sigma**2 is the prior precision weight.

  • image_edge_dt(H, W) precomputed image distance transform.

  • image_gradient_vu(H, W, 2) gradient vector image; required when use_polarity is 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 of vertices_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_rotation is 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_vu outside 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 against hypot(trial_dv - dv0, trial_du - du0) <= trust_region_px; a violation marks the step as rejected (lambda doubled, iteration counter advanced) without updating dv / 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 penalty alpha * sum(weights) * ||(dv, du) - (dv0, du0)||^2 which 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 (see LMRefineResult).

  • 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_vu must be supplied (it is already required when use_polarity is True). The reported residuals_px / weights / rms_px / covariance are 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:

LMRefineResult.

Raises:

ValueError – if any shape requirement is violated, the prior sigmas are not strictly positive, or fit_rotation is True without a positive pivot_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:
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 c are 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 that c = 4.685 corresponds 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 c is not strictly positive or residuals is 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: NavTechnique

Body-limb DT-based translation fit.

Consumes every LIMB_ARC feature 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 precision 1 / 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_specs ensures every term in confidence_spec references 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:

NavFeasibilityReport with feasible=True iff at least one LIMB_ARC has at least the configured min_arc_vertices surviving vertices.

name: ClassVar[str] = 'BodyLimbNav'

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 limb polylines.

Parameters:
  • features – Feature list filtered to the technique’s accepted types. Polylines with fewer than the configured min_arc_vertices vertices are dropped before fitting.

  • context – Per-image NavContext. Must carry image_edge_dt_ext and image_gradient_vu_ext — both populated by the orchestrator’s _make_context — plus fit_camera_rotation and max_rotation_deg.

Returns:

A NavTechniqueResult with the recovered offset, calibrated confidence, and a populated BodyLimbDiagnostics. The covariance shape and the rotation_rad / sigma_rotation_rad fields depend on context.fit_camera_rotation:

  • False (Cassini / NHLORRI default): covariance_px2 is (2, 2); rotation_rad and sigma_rotation_rad are None. Any non-(2, 2) covariance returned by lm_subpixel_refine() is logged at WARNING and truncated to the 2x2 translation block.

  • True (VGISS / GOSSI): covariance_px2 is the LM M-estimator’s (3, 3) translation + rotation information matrix; rotation_rad is the converged theta (radians) and sigma_rotation_rad is the square root of the rotation diagonal. An unexpected covariance shape is treated as a programmer error and raises RuntimeError.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and is run only on pass 2.

BodyTerminatorNav — translation fit from body terminator polylines.

Mirrors BodyLimbNav with three terminator-specific differences:

  1. The accepted feature type is TERMINATOR_ARC instead of LIMB_ARC.

  2. 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).

  3. 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: NavTechnique

Body-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_specs ensures every term in confidence_spec references 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:

NavFeasibilityReport with feasible=True iff at least one TERMINATOR_ARC has at least the configured min_arc_vertices surviving vertices.

name: ClassVar[str] = 'BodyTerminatorNav'

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 terminator polylines.

Parameters:
  • features – Feature list filtered to the technique’s accepted types. Polylines with fewer than the configured min_arc_vertices vertices are dropped before fitting.

  • context – Per-image NavContext. Must carry image_edge_dt_ext and image_gradient_vu_ext — both populated by the orchestrator’s _make_context — plus fit_camera_rotation and max_rotation_deg.

Returns:

A NavTechniqueResult with the recovered offset, calibrated confidence, and a populated BodyTerminatorDiagnostics. Per BodyLimbNav.navigate:

  • When context.fit_camera_rotation is False the result carries a (2, 2) covariance and rotation_rad / sigma_rotation_rad are None (an unexpected non-(2, 2) covariance from LM is logged at WARNING and truncated).

  • When context.fit_camera_rotation is True the result carries a (3, 3) covariance with the LM-fit rotation diagonal and populated rotation_rad / sigma_rotation_rad. An unexpected covariance shape from LM raises RuntimeError.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and 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 from feature_id). The classic example is BodyTerminatorNav (fallback) being superseded by a non-spurious BodyLimbNav or BodyDiscCorrelateNav result 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: NavTechnique

Body-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_specs ensures every term in confidence_spec references 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:

NavFeasibilityReport with feasible=True iff at least one BODY_DISC feature 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_rotation is 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, plus fit_camera_rotation / max_rotation_deg.

Returns:

A NavTechniqueResult with the recovered offset, 2x2 or 3x3 covariance, calibrated confidence, and a populated BodyDiscDiagnostics.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and is run only on pass 2.

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: NavTechnique

Body-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’s hard_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_specs ensures every term in confidence_spec references 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_BLOB with a non-zero predicted diameter — otherwise the centroid moment is degenerate.

name: ClassVar[str] = 'BodyBlobNav'

Human-readable technique name; used as the registry key.

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, and fit_camera_rotation.

Returns:

A NavTechniqueResult with the recovered offset, calibrated confidence, and a populated BodyBlobDiagnostics. The covariance shape and the rotation fields depend on context.fit_camera_rotation:

  • False (the default Cassini / NHLORRI posture): covariance_px2 is (2, 2) and rotation_rad / sigma_rotation_rad are None.

  • True (VGISS / GOSSI): covariance_px2 is the rank-deficient (3, 3) form returned by embed_rotation_unobservable() (a brightness-weighted centroid is rotation-invariant about itself, so the technique carries no rotation evidence); rotation_rad is 0.0 and sigma_rotation_rad is the unobservable sentinel.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and 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 from feature_id). The classic example is BodyTerminatorNav (fallback) being superseded by a non-spurious BodyLimbNav or BodyDiscCorrelateNav result 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: NavTechnique

Ring-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_specs ensures every term in confidence_spec references 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:

NavFeasibilityReport with feasible=True iff at least one RING_EDGE has a non-empty polyline.

name: ClassVar[str] = 'RingEdgeNav'

Human-readable technique name; used as the registry key.

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_ext and image_gradient_vu_ext — both populated by the orchestrator’s _make_context.

Returns:

A NavTechniqueResult with the recovered offset, 2x2 covariance (rank-1 when every input edge is straight), calibrated confidence, and a populated RingEdgeDiagnostics.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and is run only on pass 2.

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 +v toward +u (the unit normal is (cos, sin)) — the same convention as the sidecar schema’s ground_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_EDGE polylines are read.

Returns:

The orientation in degrees, normalised to (-90, 90], or None unless 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: NavTechnique

Ring-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_specs ensures every term in confidence_spec references 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_ANNULUS per detectable ring system in multi-planet scenes; the technique handles len(features) > 1 by Z-buffer painting all annuli into one combined template.

Parameters:

features – Feature list filtered to this technique’s accepted types.

Returns:

NavFeasibilityReport with feasible=True iff at least one RING_ANNULUS feature carries a template payload.

name: ClassVar[str] = 'RingAnnulusNav'

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 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, and fit_camera_rotation.

Returns:

A NavTechniqueResult with the recovered offset, calibrated confidence, and a populated RingAnnulusDiagnostics. The covariance shape and the rotation fields depend on context.fit_camera_rotation:

  • False (the default Cassini / NHLORRI posture): covariance_px2 is (2, 2) and rotation_rad / sigma_rotation_rad are None.

  • True: the translation NCC pyramid carries no rotation evidence, so the result reports the rank-deficient (3, 3) form returned by embed_rotation_unobservable() with rotation_rad = 0.0 and sigma_rotation_rad equal 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.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and is run only on pass 2.

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:

  1. Source detection. The image is matched-filtered against a small Gaussian PSF kernel; pixels that are local maxima above detection_sigma * image_noise_sigma are accepted; a brightness- weighted centroid pins each peak’s sub-pixel position. The brightest max_sources survivors (default 30) feed the matcher.

  2. Triplet hashing. For every unordered triplet of detected sources {A, B, C} with A brightest, 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.

  3. 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”).

  4. 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: NavTechnique

Multi-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_specs ensures every term in confidence_spec references 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:

NavTechniqueResult with the recovered offset, 2x2 covariance, calibrated confidence, and a populated StarFieldDiagnostics.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and is run only on pass 2.

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:

  1. Shifts the catalog prediction by the prior offset.

  2. Looks for a brightness peak inside a small refinement window centered on that shifted prediction.

  3. Fits a brightness-weighted moment around the peak to get a sub-pixel centroid.

  4. 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: NavTechnique

Star-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 above StarUniqueMatchNav’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_specs ensures every term in confidence_spec references a member of this set.

is_feasible(features: list[NavFeature]) NavFeasibilityReport[source]

Report feasibility based on the predictable-star cohort.

name: ClassVar[str] = 'StarRefineNav'

Human-readable technique name; used as the registry key.

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 as requires_prior=True so the orchestrator only invokes it on pass 2.

Returns:

NavTechniqueResult with the delta offset relative to the prior, a 2x2 covariance, calibrated confidence, and a populated StarRefineDiagnostics.

requires_prior: ClassVar[bool] = True

If True, this technique requires a prior offset on NavContext and is run only on pass 2.

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_mag brighter 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_ratio ambiguity gate and the brightness-margin gate both report inf when 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 within one_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: NavTechnique

Star 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_specs ensures every term in confidence_spec references 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 NavTechniqueResult with the recovered offset, calibrated confidence (capped per-mode by one_star_confidence_cap / two_star_confidence_cap), and a populated StarUniqueMatchDiagnostics. The covariance shape and rotation fields depend on the chosen path and on context.fit_camera_rotation:

  • 1-star path, fit_camera_rotation=False: (2, 2) covariance; rotation_rad and sigma_rotation_rad are None.

  • 1-star path, fit_camera_rotation=True: rank-deficient (3, 3) covariance via embed_rotation_unobservable() (a single match cannot constrain rotation); rotation_rad = 0.0 and sigma_rotation_rad is the rotation-unobservable sentinel.

  • 2-star path, fit_camera_rotation=False: (2, 2) covariance from the per-feature CRLB floor; rotation fields None.

  • 2-star path, fit_camera_rotation=True: full (3, 3) covariance with the analytic 2 * (sigma_v**2 + sigma_u**2) / L**2 rotation diagonal (L is the catalog-pair separation); rotation_rad is the Procrustes-fit angle and sigma_rotation_rad is the square root of the rotation diagonal.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and is run only on pass 2.

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: NavTechnique

Interactive 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 the ManualNavDialog, and packages the operator’s choice into a NavTechniqueResult. 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), and STAR (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.

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_PX per-axis covariance.

requires_prior: ClassVar[bool] = False

If True, this technique requires a prior offset on NavContext and is run only on pass 2.

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 ManualNavDialog so the operator can pick an offset by hand. When the operator accepts a pick, the dialog’s offset is wrapped in a full NavResult (provenance + image classifier + feature inventory + annotations populated identically to the autonomous pipeline), so callers can write the same _metadata.json and _summary.png outputs navigate_image_files produces.

Parameters:
  • obs – Loaded observation snapshot to navigate.

  • config – Optional Config override; defaults to DEFAULT_CONFIG.

Returns:

A NavResult with status='success' on accept, or None when the operator cancels or no supported overlay feature paints any pixels into the ext-FOV composite. Supported overlay types match NavTechniqueManual.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), and STAR (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.