Skip to content

Phase 9: per-instrument camera-rotation correction#121

Merged
rfrenchseti merged 5 commits into
rf_core_rewritefrom
core_rewrite_phase9
May 1, 2026
Merged

Phase 9: per-instrument camera-rotation correction#121
rfrenchseti merged 5 commits into
rf_core_rewritefrom
core_rewrite_phase9

Conversation

@rfrenchseti
Copy link
Copy Markdown
Collaborator

@rfrenchseti rfrenchseti commented Apr 30, 2026

Purpose

Phases 4–8 produced a 2-DoF (translation-only) baseline. Voyager ISS and Galileo SSI carry per-image attitude rotation residuals up to several degrees that cannot be calibrated out mission-wide, so the new pipeline must fit a small camera rotation alongside the per-image translation on those instruments. Phase 9 enables that knob, threads 3-DoF math through every technique and the ensemble combine, flips the per-camera flag on for VGISS / GOSSI, and keeps Cassini ISS / NHLORRI on the existing 2-DoF path. Closes the Phase 9 work item in AUTONAV_PLAN.md.

Changes

  • Per-instrument flag. config_410_inst_gossi.yaml and config_430_inst_vgiss.yaml flip fit_camera_rotation: true; Cassini ISS and NHLORRI stay false. The orchestrator reads fit_camera_rotation and max_rotation_deg via instrument_settings_from_obs and plumbs them onto a frozen NavContext. NavContext.with_prior now accepts either a 2x2 or a 3x3 prior covariance and projects to the 2x2 translation block before pass 2 (rotation prior carries no useful information across the pass boundary).
  • Per-technique 3-DoF math. DT techniques (BodyLimbNav, BodyTerminatorNav, RingEdgeNav) thread fit_rotation through the existing lm_subpixel_refine (vertex-centroid pivot; rotation cap drives at_edge). BodyDiscCorrelateNav runs the full 11 + 5 + 3 rotation-sample pyramid (scipy.ndimage.rotate-based template pre-rotation about the centroid-of-body-centres pivot; level-2 NCC quality curvature feeds sigma_theta). Star techniques run weighted Kabsch / orthogonal Procrustes via the new _star_helpers.similarity_transform_fit: StarFieldFromCatalogNav N-point Tukey-reweighted, StarUniqueMatchNav 2-star exact, StarRefineNav N-point on inliers. BodyBlobNav, RingAnnulusNav, and 1-star / 1-inlier paths report a rank-deficient 3x3 (translation observable, rotation unobservable) via embed_rotation_unobservable carrying the ROTATION_UNOBSERVABLE_VARIANCE = 1.0e15 finite sentinel (a literal +inf would make np.linalg.eigvalsh return NaN).
  • Ensemble combine. _combine_precision_weighted returns a typed _CombinedEstimate dataclass; _result_param_vector dispatches on covariance shape so (dv, du) and (dv, du, theta) parameter vectors flow through Mahalanobis grouping, single-link clustering, and the precision-weighted information-form merge uniformly. Mixed-DoF inputs raise a recognisable ValueError (the orchestrator pins the DoF per image so this fires only on a programmer error). Rotation propagates onto NavResult.ok / NavResult.conflicted outputs.
  • Curator + JSON. When rotation_rad is populated, the curator emits rotation_deg and sigma_rotation_deg rounded to the canonical curator confidence-precision; both fields are omitted entirely when the flag is off, so 2-DoF runs do not litter the JSON. Tier derivation (max_sigma_px) consults only the translation sigma so a rotation outcome can never inflate a tier.
  • Per-technique tunable. Every 3-DoF technique reads rotation_at_edge_fraction (default 0.95) from its config_510_techniques.yaml tuning block, parallel to at_edge_tolerance_px. ROTATION_AT_EDGE_FRACTION stays in nav.nav_technique.nav_technique as the documented default that ships in the YAML; Phase 10 calibration can retune per technique without code changes.
  • Logging + at_edge. Each DT / Procrustes technique emits an INFO log line with the converged rotation in degrees, its sigma, and an AT_EDGE annotation when |theta| >= rotation_at_edge_fraction * max_rotation_deg. The translation-side at_edge rule is unchanged; both contributions are OR-ed onto NavTechniqueResult.at_edge.
  • Documentation. New docs/developer_guide_rotation.rst covers the per-instrument flag, parameter vector, pivot rules, per-technique strategy, rank-deficient pattern, ensemble combine, JSON output, and at_edge semantics. The Phase 9 section of AUTONAV_PLAN.md is rewritten as the canonical "complete" record (matching the Phase 8 style) and the snapshot list is updated for consistency.
  • Phase 9 review folder. phase_09_review/ ships the six per-phase critique files plus an executive summary against .cursor/rules/python_best_practices.mdc, .cursor/rules/documentation.mdc, .cursor/rules/filecache_best_practices.mdc, .cursor/rules/logging_best_practices.mdc, the critique-test-suite skill, and the python-codebase-analysis skill.

Type of Change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change (fix or feature that alters existing behavior or public API)
  • Refactor (no functional or API changes)
  • Documentation
  • Tests only (no production code change)
  • CI / Build / Dependencies

(Phase 9 is primarily a new feature; the docs and tests checkboxes are checked because the change-set ships material in both areas alongside the production code.)

Testing

  • Unit tests pass
  • Integration tests pass (if applicable)
  • End-to-end tests pass (if applicable)
  • New or updated tests for changed code
  • Tested manually (describe below if applicable)

pytest -n auto --dist=loadfile reports 1230 / 1230 passing; pytest --cov retains the existing per-package coverage thresholds. Nineteen new unit tests cover the new behaviour:

  • 3-DoF ensemble combine (rotation propagation, rotation-unobservable input, mixed-DoF rejection).
  • 3x3 prior covariance acceptance on NavContext.with_prior; rotation-field defaults and propagation.
  • Per-technique 3-DoF tests on every technique that fits rotation: BodyLimbNav (3-DoF result + rotation at_edge), BodyTerminatorNav, RingEdgeNav (curved + flat-edge rank-deficient), BodyDiscCorrelateNav, BodyBlobNav (rank-deficient sentinel), StarFieldFromCatalogNav (planted rotation recovery + zero-rotation path), StarUniqueMatchNav (2-star Procrustes + 1-star unobservable), StarRefineNav (multi-inlier Procrustes + single-inlier unobservable).

Manual verification: ./scripts/run-all-checks.sh is green end-to-end (ruff check, ruff format --check, mypy --strict, pytest -n auto --dist=loadfile, sphinx-build -W -b html docs docs/_build, pymarkdown scan docs/ .cursor/ README.md CONTRIBUTING.md).

Integration / end-to-end test boxes are unchecked: integration tests are skipped by default in this branch's CI matrix (pytest -m "not integration"); the existing 2-DoF integration suite is unaffected because the new code paths only execute when fit_camera_rotation is True. A real-image VGISS / GOSSI library expansion is intentionally deferred to Phase 10 (calibration) per AUTONAV_PLAN.md.

Potential Impacts

  • Public API. NavContext gains two fields (fit_camera_rotation: bool = False, max_rotation_deg: float = 5.0), both with safe defaults so existing constructors that omit them keep the 2-DoF behaviour. NavResult.ok(...) already accepted rotation_rad / sigma_rotation_rad (shipped in Phase 0); this change populates them. New helpers (ROTATION_AT_EDGE_FRACTION, ROTATION_UNOBSERVABLE_VARIANCE, embed_rotation_unobservable, rotation_pivot_distance_px, rotation_unobservable_sigma_rad) are added to nav.nav_technique.nav_technique.__all__. _combine_precision_weighted returns a _CombinedEstimate dataclass instead of a tuple — internal helper (leading underscore), no documented external consumers.
  • Backward compatibility. Cassini ISS / NHLORRI configs stay fit_camera_rotation: false; their navigation results carry 2x2 covariances and omit rotation fields exactly as before. Voyager ISS / Galileo SSI configs flip on and now produce 3x3 covariances with rotation entries; downstream consumers (backplanes, PDS4 bundles) read translation only and are unaffected.
  • Performance. DT techniques run < 50 % slower in 3-DoF mode (LM converges in < 20 iterations including the rotation parameter). BodyDiscCorrelateNav runs ~ 19x the 2-DoF baseline (11 + 5 + 3 NCC pyramids) when fit_camera_rotation is on; this only applies to instruments where the flag is true (VGISS / GOSSI), and body-disc scenes are uncommon there. Cassini / NHLORRI compute is unchanged.
  • Downstream. No package outside this repo consumes nav_technique or nav_orchestrator internals. The _metadata.json schema gains optional rotation_deg / sigma_rotation_deg keys (omitted entirely when the flag is off), so existing JSON readers stay forward-compatible.

Checklist

  • Code follows project style (ruff check, ruff format)
  • Type annotations present and mypy passes
  • Docstrings and Sphinx docs updated (if applicable)
  • CHANGES.md updated (if user-facing change)
  • No temporary or debug code left in
  • Breaking changes flagged in Type of Change above

(CHANGES.md is not present in this repository; the canonical user-facing record for the cutover work lives in AUTONAV_PLAN.md plus the per-phase phase_NN_review/ folders.)

Notes

  • The library expansion of VGISS / GOSSI rotation-bearing scenes (Phase 9 Scope E in AUTONAV_PLAN.md) is intentionally deferred to Phase 10 alongside the wider library expansion to ~50 images and the one-time confidence-formula calibration. Phase 9 ships the math, the tunables, and the per-instrument flag flips; Phase 10 turns those knobs into calibrated numbers against ground truth.
  • Two simplifying choices in the new code that future readers may want to revisit: (1) the rotation pivot for DT techniques is the centroid of consumed polyline vertices rather than a SPICE-derived predicted body / planet centre — LimbPolyline / TerminatorPolyline / RingEdgePolyline do not currently carry the body centre and the LM fit is robust to small pivot mis-placement; (2) the integer-rounded pivot shift in _rotate_template (BodyDiscCorrelateNav) sacrifices < 1 px of pivot accuracy for a much simpler implementation, well below the 0.25 deg level-2 sample step.

🤖 Generated with Claude Code

VGISS and GOSSI flipped to fit_camera_rotation=true.  Every technique
now produces a 3x3 covariance and a populated rotation_rad /
sigma_rotation_rad pair when the per-image flag is on; the ensemble's
pinvh-based combine fuses 3-DoF results uniformly.

DT techniques run rotation as a third LM parameter via the existing
lm_subpixel_refine machinery.  BodyDiscCorrelateNav runs the
11+5+3 rotation-sample pyramid; star techniques run weighted Kabsch /
Procrustes (StarFieldFromCatalogNav N-point, StarUniqueMatchNav 2-star,
StarRefineNav N-point); centroid / 1-star paths report a rank-deficient
3x3 with the ROTATION_UNOBSERVABLE_VARIANCE sentinel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Warning

Rate limit exceeded

@rfrenchseti has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 59 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b56a17e8-4b61-449f-aa62-5299d7793a3c

📥 Commits

Reviewing files that changed from the base of the PR and between b34e9f3 and 892c2b4.

📒 Files selected for processing (11)
  • docs/developer_guide_rotation.rst
  • src/nav/nav_technique/_star_helpers.py
  • src/nav/nav_technique/nav_technique_body_blob.py
  • src/nav/nav_technique/nav_technique_body_disc.py
  • src/nav/nav_technique/nav_technique_body_limb.py
  • src/nav/nav_technique/nav_technique_body_terminator.py
  • src/nav/nav_technique/nav_technique_ring_annulus.py
  • src/nav/nav_technique/nav_technique_star_field.py
  • src/nav/nav_technique/nav_technique_star_refine.py
  • src/nav/nav_technique/nav_technique_star_unique_match.py
  • tests/nav/nav_technique/test_nav_technique_body_limb.py

Walkthrough

This PR implements Phase 9 ("Camera rotation correction per instrument") by adding optional in-plane camera rotation estimation alongside translation. Rotation fitting is configurable per-instrument via fit_camera_rotation flag; results include rotation magnitude and uncertainty. The ensemble combines 3-DoF rotation-aware results using precision-weighted averaging; rank-deficient rotation cases are handled via unobservable-variance sentinel covariance.

Changes

Cohort / File(s) Summary
Documentation & Planning
AUTONAV_PLAN.md, docs/developer_guide.rst, docs/developer_guide_rotation.rst
Updated Phase 9 status to Complete; added comprehensive rotation guide documenting per-instrument configuration, per-technique fitting approaches (DT-based LM refinement, template-NCC pyramid, star Procrustes), rank-deficiency handling, and JSON field rules.
Instrument Configuration
src/nav/config_files/config_410_inst_gossi.yaml, src/nav/config_files/config_430_inst_vgiss.yaml
Enabled fit_camera_rotation=true for Galileo SSI and Voyager ISS cameras.
Technique Tuning
src/nav/config_files/config_510_techniques.yaml
Added rotation_at_edge_fraction: 0.95 tuning parameter to all navigation technique configurations.
Core Infrastructure
src/nav/nav_orchestrator/nav_context.py, src/nav/nav_orchestrator/orchestrator.py, src/nav/nav_orchestrator/ensemble.py
Extended NavContext with fit_camera_rotation and max_rotation_deg fields; orchestrator forwards these from instrument settings; ensemble now validates and combines 3-DoF rotation-aware parameter vectors with precision weighting, rejects mixed 2-DoF/3-DoF results, and propagates rotation_rad and sigma_rotation_rad into NavResult.
Navigation Technique Foundations
src/nav/nav_technique/nav_technique.py, src/nav/nav_technique/_star_helpers.py
Added rotation-unobservability primitives (ROTATION_UNOBSERVABLE_VARIANCE, embed_rotation_unobservable, rotation_unobservable_sigma_rad, rotation_pivot_distance_px); exported SimilarityFit dataclass and similarity_transform_fit function for weighted 2D similarity transform computation.
Body-based Techniques
src/nav/nav_technique/nav_technique_body_blob.py, src/nav/nav_technique/nav_technique_body_disc.py, src/nav/nav_technique/nav_technique_body_limb.py, src/nav/nav_technique/nav_technique_body_terminator.py, src/nav/nav_technique/nav_technique_ring_annulus.py, src/nav/nav_technique/nav_technique_ring_edge.py
Integrated rotation fitting: BodyBlob/RingAnnulus embed unobservable-rotation sentinel; BodyDisc runs 3-level rotation-sampling pyramid with curvature-based uncertainty; BodyLimb/BodyTerminator/RingEdge compute pivot and pass to lm_subpixel_refine for 3-DoF LM refinement. All now return 3×3 covariance and rotation_rad/sigma_rotation_rad when enabled, with edge detection keyed to rotation magnitude.
Star-based Techniques
src/nav/nav_technique/nav_technique_star_field.py, src/nav/nav_technique/nav_technique_star_refine.py, src/nav/nav_technique/nav_technique_star_unique_match.py
Extended for 3-DoF similarity/Procrustes fitting: StarField and StarRefine conditionally perform Tukey-biweight-reweighted similarity refit; StarUniqueMatch evaluates 2-star assignments via Procrustes per assignment. All support rotation-unobservable (single-inlier) fallback and report rotation in NavTechniqueResult.
Test Configuration & Infrastructure
tests/nav/config_files/test_config_load.py, tests/nav/nav_orchestrator/test_nav_context.py, tests/nav/nav_technique/conftest.py
Updated test fixtures to verify fit_camera_rotation=True in voyager_iss/galileo_ssi configs; refactored _minimal_context and _make_nav_context to accept rotation parameters; added NavContext rotation field and 3×3 covariance-slicing behavior validation.
Orchestrator Tests
tests/nav/nav_orchestrator/test_ensemble.py
Added three ensemble tests: 3-DoF agreement/combination validation with precision-weighted rotation mean (~0.011 rad), rotation-unobservable contribution negligibility, and mixed 2-DoF/3-DoF rejection.
Technique Tests
tests/nav/nav_technique/test_nav_technique_body_blob.py, tests/nav/nav_technique/test_nav_technique_body_disc.py, tests/nav/nav_technique/test_nav_technique_body_limb.py, tests/nav/nav_technique/test_nav_technique_body_terminator.py, tests/nav/nav_technique/test_nav_technique_ring_edge.py, tests/nav/nav_technique/test_nav_technique_star_field.py, tests/nav/nav_technique/test_nav_technique_star_refine.py, tests/nav/nav_technique/test_nav_technique_star_unique_match.py
Added end-to-end 3-DoF tests for each technique verifying 3×3 covariance output, rotation recovery (near zero for zero-rotation scenes or matching planted values), sigma_rotation_rad validity, edge-detection under rotation saturation, and rank-1 geometry preservation where applicable.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: Phase 9 implements per-instrument camera-rotation correction as the primary feature across the entire codebase.
Docstring Coverage ✅ Passed Docstring coverage is 89.22% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering all required sections: purpose with issue closure, detailed changes across configuration/math/ensemble/curator/logging/documentation, type of change, testing summary with specific counts and technique coverage, potential impacts with API/compatibility/performance analysis, full checklist completion, and relevant notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 24 minutes and 59 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@rfrenchseti rfrenchseti changed the base branch from main to rf_core_rewrite April 30, 2026 23:43
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 83.86412% with 76 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (rf_core_rewrite@e7fb581). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/nav/nav_technique/nav_technique_body_disc.py 88.61% 7 Missing and 7 partials ⚠️
src/nav/nav_technique/_star_helpers.py 76.74% 5 Missing and 5 partials ⚠️
...v/nav_technique/nav_technique_star_unique_match.py 85.93% 6 Missing and 3 partials ⚠️
src/nav/nav_technique/nav_technique_star_field.py 85.45% 4 Missing and 4 partials ⚠️
src/nav/nav_technique/nav_technique_body_limb.py 68.18% 4 Missing and 3 partials ⚠️
...nav/nav_technique/nav_technique_body_terminator.py 68.18% 4 Missing and 3 partials ⚠️
src/nav/nav_technique/nav_technique_star_refine.py 87.50% 3 Missing and 3 partials ⚠️
src/nav/nav_technique/nav_technique_ring_edge.py 76.19% 2 Missing and 3 partials ⚠️
src/nav/nav_orchestrator/ensemble.py 88.23% 2 Missing and 2 partials ⚠️
src/nav/nav_orchestrator/nav_context.py 60.00% 1 Missing and 1 partial ⚠️
... and 2 more
Additional details and impacted files
@@                Coverage Diff                 @@
##             rf_core_rewrite     #121   +/-   ##
==================================================
  Coverage                   ?   54.66%           
==================================================
  Files                      ?      135           
  Lines                      ?    16894           
  Branches                   ?     2283           
==================================================
  Hits                       ?     9235           
  Misses                     ?     7042           
  Partials                   ?      617           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rfrenchseti
Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

rfrenchseti and others added 3 commits April 30, 2026 16:46
Addresses the post-merge review findings:

- Add ``ROTATION_AT_EDGE_FRACTION`` shared constant in
  ``nav.nav_technique.nav_technique`` and use it in every 3-DoF
  technique instead of the inline ``0.95`` magic number.
- Hoist every inline ``ROTATION_UNOBSERVABLE_VARIANCE`` /
  ``embed_rotation_unobservable`` import in star techniques and tests
  to the module-level import block.
- ``embed_rotation_unobservable`` now validates input shape;
  ``similarity_transform_fit`` rejects negative weights up front.
- Reconcile the AUTONAV_PLAN snapshot list with the Phase 9
  ``(complete)`` heading; reword the curator-precision note to
  reference ``CONFIDENCE_DECIMALS`` instead of inlining "3 decimals".
- DT techniques now reuse the already-computed ``sigma_rotation_rad``
  for the rotation INFO line, validate covariance shape before any
  array access, log a warning when ``lm_subpixel_refine`` returns an
  unexpected shape with ``fit_rotation=False``, and guard the navigate
  path against an empty vertex aggregation.
- ``BodyDiscCorrelateNav`` raises on a non-(2,2) NCC covariance,
  guards ``_composite_pivot_vu`` against an empty feature list, drops
  the unused ``winner`` argument from
  ``_rotation_sigma_from_quality``, and corrects the level-0 sample-
  step description (the ``np.linspace`` step is
  ``2 * max_rotation_deg / 10``, exactly 1 deg only at the default).
- Expanded Google-style docstrings for ``rotation_pivot_distance_px``,
  ``rotation_unobservable_sigma_rad``, ``embed_rotation_unobservable``,
  ``BodyBlobNav._fail_no_signal``, ``BodyDiscCorrelateNav._ncc_at_rotation``,
  ``BodyDiscCorrelateNav._evaluate_rotation_samples``,
  ``BodyDiscCorrelateNav._rotation_sigma_from_quality``,
  ``StarUniqueMatchNav._similarity_fit_assignment``,
  ``StarUniqueMatchNav._build_two_star_covariance_3dof``, and the
  ``_rotate_about`` test helper.
- Tests: replace vacuous ``assert ... if hasattr(...)`` with a
  direct status check, tighten rotation tolerances toward the
  expected zero-rotation outcome, replace fragile ``> 0`` checks
  with ``pytest.approx`` against the analytic combined sigma, refactor
  ``_minimal_context`` to accept kwargs so ``test_navcontext_rotation_fields_propagate``
  no longer rebuilds the dataclass, hoist ``import math`` /
  ``import pytest`` in test files, and add the missing
  ``spurious is False`` / Procrustes-translation assertions to the
  3-DoF star unique-match tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each 3-DoF technique now reads ``rotation_at_edge_fraction`` from its
``config_510_techniques.yaml`` tuning block (default 0.95) instead of
referencing the module-level ``ROTATION_AT_EDGE_FRACTION`` constant
directly.  The constant stays as the documented default value that
ships in the YAML, parallel to ``at_edge_tolerance_px``.  Phase 10
calibration can now retune the rotation-edge fraction per technique
without touching code; ``BodyDiscCorrelateNav`` gains its first
``tuning`` block to carry the new key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/nav/nav_orchestrator/ensemble.py (1)

504-514: ⚠️ Potential issue | 🟠 Major

Add rotation_rad and sigma_rotation_rad parameters to NavResult.conflicted() and thread them through in the ensemble conflicted branch.

The NavResult.ok() builder accepts and forwards rotation fields, but NavResult.conflicted() does not include them in its signature. If a 3-DoF ensemble reaches the conflicted branch (lines 504–514), the combined.rotation_rad and computed sigma_rotation_rad are discarded, leaving a 3×3 covariance without the required rotation metadata. This violates the contract enforced in _result_param_vector() (line 136–138), which requires 3×3 covariance to have rotation_rad set.

Update NavResult.conflicted() to accept rotation_rad: float | None = None and sigma_rotation_rad: float | None = None, pass them to the returned instance, and forward them in the ensemble conflicted branch. Add a regression test for 3-DoF conflicted outcomes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nav/nav_orchestrator/ensemble.py` around lines 504 - 514,
NavResult.conflicted currently ignores rotation metadata; update its signature
to accept rotation_rad: float | None = None and sigma_rotation_rad: float | None
= None, store these on the returned NavResult instance (same as NavResult.ok
does), and ensure the ensemble conflicted branch (where
NavResult.conflicted(...) is returned around combined.offset_px) forwards
combined.rotation_rad and the computed sigma_rotation_rad into those new
parameters; also add a regression test asserting a 3-DoF conflicted outcome
produces a 3×3 covariance and non-None rotation_rad/sigma_rotation_rad and that
_result_param_vector() no longer raises for this case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/developer_guide_rotation.rst`:
- Around line 167-174: The docs currently state the rotation-edge threshold as a
hard-coded 0.95 * max_rotation_deg; update the text to reference the
configurable tuning parameter by describing the condition as
rotation_at_edge_fraction * max_rotation_deg (where rotation_at_edge_fraction is
read from each technique's tuning, defaulting to 0.95), and keep the rest of the
sentence about OR-ing with translation at_edge and the INFO log for
fit_camera_rotation and NavTechniqueResult.at_edge unchanged so the guide
matches the implementation.
- Around line 129-131: The docs incorrectly label ROTATION_UNOBSERVABLE_VARIANCE
as having units "px²"; update the documentation text for the constant
ROTATION_UNOBSERVABLE_VARIANCE to state the correct units "rad²" (angular
variance) and clarify that it occupies the rotation slot of the 3x3 covariance
matrix so readers understand it's an angular variance sentinel used in rotation,
not a pixel variance. Ensure the amended sentence replaces "px²" with "rad²" and
retains the note about it being a large finite sentinel for +inf used with
np.linalg.eigvalsh.

In `@src/nav/nav_orchestrator/nav_context.py`:
- Around line 86-87: NavContext now exposes fit_camera_rotation and
max_rotation_deg but lacks validation; add a __post_init__ on class NavContext
that (1) ensures fit_camera_rotation is a bool (raise TypeError if not), (2)
ensures max_rotation_deg is a finite number (int/float) and >= 0 (raise
TypeError for wrong type, ValueError for negative or NaN/inf), and (3) coerce
ints to float for max_rotation_deg if valid; reference the NavContext class, the
fit_camera_rotation and max_rotation_deg attributes, and implement checks and
raises inside __post_init__ to enforce the public API contract.

In `@src/nav/nav_technique/nav_technique_body_blob.py`:
- Around line 399-406: Update the navigate() docstring to describe the new
rotation-aware return format: explain that the function can now return either a
2x2 or 3x3 covariance matrix (depending on whether rotation is fit), that
rotation_rad and sigma_rotation_rad may be present (or None) on the result, and
clarify how confidence, spurious, at_edge, diagnostics, and covariance_px2 are
represented in both cases; also mirror this docstring change where similar
documentation appears around the other instance noted (lines ~451-453) so both
places accurately describe the 2x2 vs 3x3 behavior and the optional rotation
fields.

In `@src/nav/nav_technique/nav_technique_body_disc.py`:
- Around line 448-467: The sampling can exceed ±max_rotation_deg and sigma is
computed at the middle sample regardless of which sample won; update the
sample-generation and sigma computation: when building l1_thetas and l2_thetas
(and the analogous block at lines ~627-644) clamp each candidate theta to lie
within +/- math.radians(max_rotation_deg) from the nominal base (e.g.,
l0_winner.theta_rad or l1_winner.theta_rad) before passing to
_evaluate_rotation_samples, and change _rotation_sigma_from_quality usage to
compute sigma around the actual winner index (use the winner's rank/index and
fit curvature using that winner and its immediate neighbors) rather than always
using the middle sample so the returned sigma corresponds to the returned theta.
- Around line 109-132: The current use of np.roll (around variables
shift_v/shift_u on template_img/template_mask) causes circular wraparound;
replace each np.roll-based shift with explicit zero-filled translation: compute
source and destination slice ranges from shift_v/shift_u, create zero arrays
matching template_img/template_mask, copy only the overlapping window
(respecting dtypes—float for template_img, uint8 for template_mask) into the
destination so out-of-bounds pixels are dropped rather than wrapped, then pass
those zero-padded arrays to ndimage_rotate (same angle=math.degrees(theta_rad)
and order/mode settings) and finally reverse the translation after rotation
using the same crop/pad logic to realign rotated and rotated_mask with the
original frame.

In `@src/nav/nav_technique/nav_technique_body_limb.py`:
- Around line 262-282: Update the navigate() docstring to reflect the new return
contract: document that when fit_rotation is True the method returns
rotation_rad and sigma_rotation_rad and a (3, 3) covariance
(translation+rotation), and when fit_rotation is False it returns a (2, 2)
covariance with rotation_rad and sigma_rotation_rad set to None; mention types
(float | None) for rotation_rad and sigma_rotation_rad and clarify covariance
shape behavior and truncation (covariance[:2,:2]) so callers know to handle both
2x2 and 3x3 covariance outputs.

In `@src/nav/nav_technique/nav_technique_body_terminator.py`:
- Around line 281-301: The docstring for navigate() on BodyTerminatorNav is
stale: update its documented return contract to reflect the new rotation-aware
output (when fit_rotation=True) which now returns rotation_rad and
sigma_rotation_rad and a (3, 3) covariance; explicitly document both modes
(fit_rotation=True -> 3-DoF: position, rotation_rad: float, sigma_rotation_rad:
float, covariance (3,3); fit_rotation=False -> 2-DoF: position, covariance (2,2)
and rotation fields as None). Mention types and when rotation fields are None,
and update any examples and type hints in the navigate() docstring accordingly.

In `@src/nav/nav_technique/nav_technique_ring_annulus.py`:
- Around line 211-217: The navigate() docstring still claims it returns only 2x2
covariance but the code conditionally returns a 3x3 covariance and a rotation
field when context.fit_camera_rotation is true; update the navigate() docstring
to state that the returned 'covariance' may be 2x2 or 3x3 depending on
fit_camera_rotation and that a 'rotation' (or rotation-related) field is
included in the 3-DoF case; also update the second docstring block around the
other navigate-related return docs (the other docstring referenced near the
covariance handling) to match this 2-DoF vs 3-DoF contract and mention the shape
and types for both branches.

In `@src/nav/nav_technique/nav_technique_star_field.py`:
- Around line 715-719: The INFO log currently appends ', AT_EDGE' only when
rotation_at_edge is true but not when the translation search hit the boundary
(at_edge), so update the logging in the self.logger.info call that prints
Rotation (the call referencing rotation_rad, sigma_rotation_rad,
rotation_at_edge) to also include the marker when at_edge is true (i.e., use ',
AT_EDGE' if rotation_at_edge or at_edge else ''), or alternatively ensure the
offset/translation INFO line (the one logging offset_x/offset_y or similar)
appends ', AT_EDGE' when at_edge is true so both rotation- and translation-edge
results consistently show the AT_EDGE suffix.

In `@src/nav/nav_technique/nav_technique_star_refine.py`:
- Around line 272-306: The branch that calls _fit_rotation_3dof updates
offset_v_total and offset_u_total but doesn't recompute at_edge, leaving it
based on the pre-refit delta+prior; after the call to _fit_rotation_3dof (inside
the if fit_rotation block) recompute at_edge using the updated offset_v_total
and offset_u_total with the same search-margin check used earlier (i.e., compare
the updated offset magnitude against the search margin threshold that was used
to set at_edge previously), and then use that recomputed at_edge value for the
subsequent logic and logging (affecting the rotation_at_edge flag and AT_EDGE
marker).

In `@src/nav/nav_technique/nav_technique_star_unique_match.py`:
- Around line 185-189: Update the docstring for the return value in
nav_technique_star_unique_match (specifically describing NavTechniqueResult from
_try_two_star and the 1-star path) to separately document the 1-star and 2-star
cases: state that the 1-star case returns offset, a 2x2 covariance (rotation
unobservable), calibrated confidence, and StarUniqueMatchDiagnostics; state that
the 2-star case now returns offset plus an observed rotation (rotation_rad), its
uncertainty (sigma_rotation_rad), and a full 3x3 covariance reflecting rotation
observability, along with calibrated confidence and populated
StarUniqueMatchDiagnostics; ensure the wording matches the actual returned
fields (rotation_rad, sigma_rotation_rad, 2x2 vs 3x3 covariance) and mention the
fit_camera_rotation behavior only where still applicable.

In `@src/nav/nav_technique/nav_technique.py`:
- Around line 97-103: The helper embed_rotation_unobservable currently only
validates shape; update it to also reject non-finite covariance entries by
checking np.isfinite on the converted array (arr or cov_2x2) and raise a clear
ValueError if any element is NaN/inf; keep the existing shape check, then
perform the finiteness check before building out and assigning
ROTATION_UNOBSERVABLE_VARIANCE so callers get an immediate, explicit error
instead of downstream linear-algebra failures.

In `@tests/nav/nav_technique/test_nav_technique_body_limb.py`:
- Around line 343-363: The test currently forges a fixed 4.9° rotation which
hardcodes the old 0.95*5.0 cutoff; update the test to derive the forged angle
from the technique tuning by reading rotation_at_edge_fraction from
BodyLimbNav.tuning and multiplying by the local max_rotation_deg (or the test's
max_rotation_deg constant) and use that product converted to radians for
LMRefineResult.rotation_rad and the assert; adjust the monkeypatch/forged_result
construction to compute rotation_rad = np.deg2rad(rotation_at_edge_fraction *
max_rotation_deg) so future retunes won't break the test.

---

Outside diff comments:
In `@src/nav/nav_orchestrator/ensemble.py`:
- Around line 504-514: NavResult.conflicted currently ignores rotation metadata;
update its signature to accept rotation_rad: float | None = None and
sigma_rotation_rad: float | None = None, store these on the returned NavResult
instance (same as NavResult.ok does), and ensure the ensemble conflicted branch
(where NavResult.conflicted(...) is returned around combined.offset_px) forwards
combined.rotation_rad and the computed sigma_rotation_rad into those new
parameters; also add a regression test asserting a 3-DoF conflicted outcome
produces a 3×3 covariance and non-None rotation_rad/sigma_rotation_rad and that
_result_param_vector() no longer raises for this case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d55b64d-a72f-445e-90f7-790e7aba2b47

📥 Commits

Reviewing files that changed from the base of the PR and between e7fb581 and b34e9f3.

📒 Files selected for processing (32)
  • AUTONAV_PLAN.md
  • docs/developer_guide.rst
  • docs/developer_guide_rotation.rst
  • src/nav/config_files/config_410_inst_gossi.yaml
  • src/nav/config_files/config_430_inst_vgiss.yaml
  • src/nav/config_files/config_510_techniques.yaml
  • src/nav/nav_orchestrator/ensemble.py
  • src/nav/nav_orchestrator/nav_context.py
  • src/nav/nav_orchestrator/orchestrator.py
  • src/nav/nav_technique/_star_helpers.py
  • src/nav/nav_technique/nav_technique.py
  • src/nav/nav_technique/nav_technique_body_blob.py
  • src/nav/nav_technique/nav_technique_body_disc.py
  • src/nav/nav_technique/nav_technique_body_limb.py
  • src/nav/nav_technique/nav_technique_body_terminator.py
  • src/nav/nav_technique/nav_technique_ring_annulus.py
  • src/nav/nav_technique/nav_technique_ring_edge.py
  • src/nav/nav_technique/nav_technique_star_field.py
  • src/nav/nav_technique/nav_technique_star_refine.py
  • src/nav/nav_technique/nav_technique_star_unique_match.py
  • tests/nav/config_files/test_config_load.py
  • tests/nav/nav_orchestrator/test_ensemble.py
  • tests/nav/nav_orchestrator/test_nav_context.py
  • tests/nav/nav_technique/conftest.py
  • tests/nav/nav_technique/test_nav_technique_body_blob.py
  • tests/nav/nav_technique/test_nav_technique_body_disc.py
  • tests/nav/nav_technique/test_nav_technique_body_limb.py
  • tests/nav/nav_technique/test_nav_technique_body_terminator.py
  • tests/nav/nav_technique/test_nav_technique_ring_edge.py
  • tests/nav/nav_technique/test_nav_technique_star_field.py
  • tests/nav/nav_technique/test_nav_technique_star_refine.py
  • tests/nav/nav_technique/test_nav_technique_star_unique_match.py

Comment thread docs/developer_guide_rotation.rst
Comment thread docs/developer_guide_rotation.rst
Comment thread src/nav/nav_orchestrator/nav_context.py
Comment thread src/nav/nav_technique/_star_helpers.py Outdated
Comment thread src/nav/nav_technique/nav_technique_body_blob.py
Comment thread src/nav/nav_technique/nav_technique_star_field.py Outdated
Comment thread src/nav/nav_technique/nav_technique_star_refine.py
Comment thread src/nav/nav_technique/nav_technique_star_unique_match.py Outdated
Comment thread src/nav/nav_technique/nav_technique.py
Comment thread tests/nav/nav_technique/test_nav_technique_body_limb.py Outdated
Addresses the second round of review findings on PR #121:

- ``_rotate_template`` (BodyDiscCorrelateNav) replaces the np.roll
  shift with a zero-padded translate via the new ``_zero_padded_shift``
  helper so out-of-bounds pixels drop instead of wrapping to the
  opposite edge.  Wraparound was a theoretical bug for body templates
  whose support sits anywhere off the array centre — the second shift
  back would smear template content across the image after rotation.
- The level-1 and level-2 rotation samples are clamped to
  ``[-max_rotation_rad, +max_rotation_rad]`` so the outer-loop search
  never proposes a rotation outside the operator-supplied envelope.
- ``_rotation_sigma_from_quality`` regains the ``winner`` parameter
  and returns ``None`` when the level-2 winner is at one of the side
  samples (the 3-sample finite-difference parabola is only valid when
  the winner is at the centre).  Edge-winners fall back to the
  rotation-unobservable sentinel rather than report a curvature taken
  at the wrong centre.
- Star-refine recomputes ``at_edge`` after the Procrustes refit since
  the refit can pull the absolute offset outside the search window
  even when the delta-based check was False.
- Star-field's rotation INFO log now annotates ``AT_EDGE`` when either
  the rotation cap or the translation margin trips the flag, matching
  the joint behaviour of the result's ``at_edge`` field.
- BodyBlobNav, BodyLimbNav, BodyTerminatorNav, RingAnnulusNav, and
  StarUniqueMatchNav navigate / _fail_no_signal docstrings now
  describe the 2-DoF vs 3-DoF return contract: covariance shape,
  ``rotation_rad`` / ``sigma_rotation_rad`` semantics, and the
  rank-deficient-rotation behaviour where applicable.
- ``SimilarityFit`` docstring corrected: the implementation stores
  ``dc - R @ cc`` (global-frame translation), not the pivot-aware
  ``R(c - pivot) + pivot + translation`` form the previous text
  described.  The pivot is reported alongside so callers can recover
  the pivot-relative translation explicitly.
- ``developer_guide_rotation.rst`` references the configurable
  ``rotation_at_edge_fraction`` instead of a hardcoded ``0.95``.
- ``test_body_limb_nav_3dof_at_edge_when_rotation_saturates`` derives
  the forged rotation angle from
  ``BodyLimbNav.tuning['rotation_at_edge_fraction']`` so future
  retunes do not break the test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rfrenchseti rfrenchseti merged commit e5fe147 into rf_core_rewrite May 1, 2026
8 checks passed
@rfrenchseti rfrenchseti deleted the core_rewrite_phase9 branch May 1, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant