Skip to content

Phase 6: Ring-annulus technique + per-technique config#117

Merged
rfrenchseti merged 3 commits into
rf_core_rewritefrom
core_rewrite_phase6
Apr 30, 2026
Merged

Phase 6: Ring-annulus technique + per-technique config#117
rfrenchseti merged 3 commits into
rf_core_rewritefrom
core_rewrite_phase6

Conversation

@rfrenchseti
Copy link
Copy Markdown
Collaborator

@rfrenchseti rfrenchseti commented Apr 30, 2026

Purpose

Phase 6 of the autonav rewrite ships RingAnnulusNav so distant Saturn / Uranus / Jupiter / Neptune ring scenes — where the entire ring system spans only a handful of pixels — finally have a viable navigation technique. Closes the design's "low-resolution ring annulus" path, retires every per-technique magic constant in favor of config_510_techniques.yaml, and fixes two pre-existing crashes (tiny-moon filter_downsample assertion, --nav-models 'rings' glob mismatch) that surfaced while exercising the new path against real holdings.

Changes

  • Ship RingAnnulusNav: pyramid-NCC fit on RING_ANNULUS features with use_gradient='auto', Z-buffer paint of multi-planet annulus features, and a full diagnostics + confidence-spec wiring matching the BodyDiscCorrelateNav shape.
  • The rings model collapses every annulus-eligible per-ring rendering into one composite RING_ANNULUS per planet (constituent_count = number of fused rings) so the reliability formula's constituent_count term scales with the size of the ring system. A new _composite_ring_renderings helper unions per-ring masks (logical OR) and takes the per-pixel max of the rendered images.
  • Per-planet ring-annulus emission gates: feature_emission.ring_annulus.{default, planets.SATURN, JUPITER, URANUS, NEPTUNE} carry max_radial_px and kmpp_threshold; the per-polyline gate fires when a single edge compresses below max_radial_px and the system-level gate fires when the entire ring system spans only a handful of pixels (km_per_pixel_radial >= kmpp_threshold).
  • Ring filter pass-4 outermost-preservation: at very low resolution every neighboring fade overlaps every other and the per-edge check could drop every outer-side edge; restore the feature carrying the largest in-range edge so navigation has at least one outer reference. The restore replaces the trimmed entry by key — never appends a duplicate — so the orchestrator's per-feature accounting holds.
  • config_510_techniques.yaml is the single source of truth for every NavTechnique's confidence-formula coefficients (load_confidence_spec) and runtime tunables (load_technique_tuning) — spurious-detection thresholds, at-edge tolerance, minimum arc length, plus the per-planet feature_emission.ring_annulus block. Module-level Python constants for those values are deleted; missing fields raise ConfidenceConfigError at config-load time so a YAML typo fails fast at process startup.
  • RingAnnulusNav.navigate raises ValueError with a descriptive message when every input feature lacks a template payload, matching the public API boundary.
  • _upsample_factor validates config.offset.correlation_fft_upsample_factor: rejects bool, non-numeric, non-finite, and out-of-range values; bounded at [1, 1_000_000] to prevent FFT memory blow-ups.
  • _search_window_for_obs hard-fails on a missing extfov_margin_vu attribute across all six techniques (was a silent 32x32 fallback).
  • Adaptive PNG whitepoint: the summary-PNG stretch scales the bright-pixel clip count to 5% of detected outliers (median + 15*MAD threshold) instead of a fixed 0.1% of all pixels. Dark-sky images with a few bright stars no longer saturate every star to 255.
  • Pre-existing nav_model_body.py filter_downsample crash on tiny moons (JANUS / PANDORA) fixed: split the empty-silhouette path so a downsampled-shape km/pixel array is no longer fed back through filter_downsample.
  • --nav-models 'rings' / 'body:saturn' glob normalization: bare prefixes expand to prefix:* and the value after a colon is uppercased to match the prefix:VALUE registry naming convention.
  • Validate vol_start / vol_end arguments to DataSetPDS3 against the available volume list and raise a descriptive ValueError on a typo.
  • Pytest excludes integration tests by default via addopts = ["-m", "not integration"]; scripts/run-all-checks.sh gains a -i / --integration flag and prints a one-line message indicating whether integration tests will run. CI's run-tests.yml adopts the same default.

Type of Change

  • Bug fix (non-breaking) — JANUS / PANDORA filter_downsample crash; --nav-models 'rings' glob mismatch
  • New feature (non-breaking) — RingAnnulusNav and per-planet annulus emission
  • Breaking change (fix or feature that alters existing behavior or public API)
  • Refactor (no functional or API changes) — magic constants moved to YAML; per-technique tuning blocks
  • Documentation — developer_guide_techniques.rst, developer_guide_navigation_models_rings.rst, user_guide_navigation.rst, introduction_configuration.rst, CLAUDE.md, CONTRIBUTING.md, PHASE6_LIBRARY_SEED.md
  • Tests only (no production code change) — new test_nav_technique_ring_annulus.py, test_confidence_config.py; extended rings / orchestrator / body-technique / PNG tests
  • CI / Build / Dependencies — addopts = ["-m", "not integration"] default; CI run-tests.yml updated

Testing

  • Unit tests pass — 1169 pass, 10 integration tests deselected by the new default
  • Integration tests pass (if applicable) — pass -m "" or -m integration to opt in; the two new operator-verified sidecars (N1447064164, W1444747627) are covered by the existing parameterized regression test
  • End-to-end tests pass (if applicable)
  • New or updated tests for changed code — test_nav_technique_ring_annulus.py (16 tests covering planted-offset recovery, multi-planet Z-buffer paint, infeasibility, at-edge, hard-zero confidence, diagnostics, empty-input ValueError, and the four _upsample_factor validation paths); test_confidence_config.py (20 tests covering bundled-YAML round-trip, malformed input, and load_technique_tuning); rings / orchestrator / body-technique / PNG test files extended to cover the planet-specific annulus gate, the composite-per-planet emission, the outermost-preservation pass + duplicate-key replace, the model-glob normalization, and the adaptive PNG stretch.
  • Tested manually — verified end-to-end against N1447064164 (km/px=2108) and W1444747627 (km/px=23067): the rings model now emits a single composite RING_ANNULUS per planet, the reliability gate accepts it, and RingAnnulusNav runs.

Potential Impacts

  • Public APIRingAnnulusNav is added to nav.nav_technique.__all__; RING_ANNULUS_MAX_RADIAL_PX and RING_ANNULUS_KMPP_THRESHOLD are removed from nav.nav_model.nav_model_rings.__all__ (operators tuning these values now edit config_510_techniques.yaml); LIMB_MIN_ARC_PX, TERMINATOR_MIN_ARC_PX, SPURIOUS_*, and _AT_EDGE_TOLERANCE_PX are deleted from per-technique modules and replaced by per-technique tuning: YAML blocks read via cls.tuning.
  • Backward compatibility — The integration-test default change (pytest no longer collects integration tests) affects CI matrix steps that relied on the old default. CI's run-tests.yml and scripts/run-all-checks.sh are updated to reflect the new convention.
  • Performance — One extra np.median + np.median(np.abs(...)) per summary PNG (negligible against the existing FFT pyramid); composite annulus rendering is cheaper than per-ring annulus rendering since the OR-and-max is one pass over each ext-FOV array.
  • Downstream — Any operator config that set the now-removed Python constants must move the value to config_510_techniques.yaml (operator runbook in PHASE6_LIBRARY_SEED.md).

Checklist

  • Code follows project style (ruff check, ruff format)
  • Type annotations present and mypy passes (274 source files clean)
  • Docstrings and Sphinx docs updated (Sphinx -W build green; pymarkdown scan clean)
  • CHANGES.md updated (if user-facing change) — N/A, no CHANGES.md in this repo
  • No temporary or debug code left in
  • Breaking changes flagged in Type of Change above — only the magic-constant move is a structural change; documented in Potential Impacts

Notes

  • phase_06_review/ (the seven CRITIQUE_*.md files for the per-phase definition of done) is intentionally excluded from this PR per request.
  • Library expansion (Phase 6 §B): two operator-curated sidecars for distant Saturn ring views ship in this PR; broader low-res ring coverage tracked in PHASE6_LIBRARY_SEED.md.

🤖 Generated with Claude Code

- Ship RingAnnulusNav: NCC against multi-ring composite RING_ANNULUS templates with use_gradient='auto', Z-buffer paint of multi-planet annulus features, and a full diagnostics + confidence-spec wiring matching the BodyDiscCorrelateNav shape.
- Ship config_510_techniques.yaml carrying every NavTechnique's confidence-formula coefficients plus per-technique runtime tunables (spurious thresholds, at-edge tolerance, minimum arc length) plus the planet-specific feature_emission.ring_annulus block (Saturn / Jupiter / Uranus / Neptune).  Module-level magic constants for those values are deleted; load_confidence_spec / load_technique_tuning validate the YAML at config-load time and raise ConfidenceConfigError on malformed input.
- Add system-level RING_ANNULUS emission gate keyed on per-planet km/px threshold so distant-Saturn ring scenes (where the entire ring system spans only a few pixels) emit one annulus per surviving feature rather than per-edge polylines.
- Add ring filter pass-4 outermost-preservation: at very low resolution every neighboring fade overlaps every other and the per-edge check could drop every outer-side edge; restore the feature carrying the largest in-range edge so navigation has at least one outer reference (typically the A-ring outer edge for Saturn).
- Fix --nav-models 'rings' / 'body:saturn' glob normalization: bare prefixes expand to prefix:* and the value after a colon is uppercased to match the prefix:VALUE registry naming convention.
- Fix pre-existing nav_model_body.py filter_downsample crash on tiny moons (JANUS / PANDORA): split the empty-silhouette path so a downsampled-shape km/pixel array is no longer fed back through filter_downsample.
- Disable integration tests in CI via -m 'not integration': operator-curated image-library regression tests rely on real PDS3 holdings + SPICE kernels and are slow / flaky in CI.  Run locally before merging.
- Update docs/introduction_configuration.rst, docs/developer_guide_techniques.rst, and docs/user_guide_navigation.rst for the new config design and the shipped RingAnnulusNav.
- New unit-test files: test_nav_technique_ring_annulus.py (9 tests), test_confidence_config.py (20 tests).  Existing rings / orchestrator / body-technique test files extended to cover the planet-specific annulus gate, the outermost-preservation pass, and the model-glob normalization.
- Add PHASE6_LIBRARY_SEED.md operator runbook and two new operator-verified sidecars for distant Saturn ring views (N1447064164, W1444747627).

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 34 minutes and 38 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: 7dc99ef0-4dbb-4b80-9bcf-df570917d228

📥 Commits

Reviewing files that changed from the base of the PR and between 5d90df5 and 6113be4.

📒 Files selected for processing (6)
  • src/nav/config/config.py
  • src/nav/dataset/dataset_pds3.py
  • src/nav/nav_model/rings/ring_filter.py
  • src/nav/navigate_image_files.py
  • tests/nav/nav_model/test_nav_model_rings.py
  • tests/nav/nav_model/test_nav_model_rings_integration.py

Walkthrough

Adds a new RingAnnulusNav technique, moves per-technique confidence coefficients and tuning into a YAML registry loaded at config-read time, refactors techniques to use config-backed tuning, updates ring-model emission/filtering to support composite annuli, adds tests/docs/sidecars, and adjusts CI to exclude integration tests by default.

Changes

Cohort / File(s) Summary
CI & Test Runner
.github/workflows/run-tests.yml, pyproject.toml, scripts/run-all-checks.sh, CONTRIBUTING.md
Exclude integration tests by default (-m "not integration" / pytest addopts), add --integration runner flag, and tighten Codecov upload condition to skip fork PRs.
New Technique & Exports
src/nav/nav_technique/nav_technique_ring_annulus.py, src/nav/nav_technique/__init__.py
Adds RingAnnulusNav implementation and exports it from the package.
Confidence / Tuning Config
src/nav/nav_technique/confidence_config.py, src/nav/config_files/config_510_techniques.yaml, src/nav/nav_technique/nav_technique.py
New loader load_confidence_spec/load_technique_tuning with ConfidenceConfigError; introduces tuning class var and documents YAML-driven confidence/tuning schema.
Config plumbing
src/nav/config/config.py
_validate_registered_techniques converted to instance method and now assigns confidence_spec/tuning to registered techniques and runs cross-term validation.
Technique refactors (tuning & confidence removal)
src/nav/nav_technique/nav_technique_body_* (_disc.py, _blob.py, _limb.py, _terminator.py, _ring_edge.py)
Remove module-level hardcoded confidence/tuning; techniques read tuning/confidence from config at init and rely on self.confidence_spec/self.tuning instance values; tighten _search_window_for_obs to require extfov_margin_vu.
Ring model & filter changes
src/nav/nav_model/nav_model_rings.py, src/nav/nav_model/rings/ring_filter.py, src/nav/nav_model/nav_model_body.py
Replace global RING_ANNULUS_MAX_RADIAL_PX with per-planet config params and system kmpp gate; composite per-planet annulus creation; restore outermost in-range edge during pass-4 fade conflict; avoid unnecessary downsampling for empty silhouettes.
Orchestrator & model selection
src/nav/nav_orchestrator/orchestrator.py
Normalize only_models patterns (prefix:VALUE normalization, uppercase RHS, wildcard expansion for prefix-only tokens) before filtering.
Image rendering / UI stretch
src/nav/navigate_image_files.py, tests/nav/test_navigate_image_files.py
Change grayscale-to-RGB quantile stretch to adapt white quantile using robust bright-outlier estimate; add tests covering outlier/abundant-bright behavior.
Dataset behavior
src/nav/dataset/dataset_pds3.py
Preserve user --volumes order and validate vol_start/vol_end are in known volumes, raising ValueError for invalid names.
Tests & Integration data
tests/... (many files — see below)
Add/modify many unit and integration tests: confidence loader tests, RingAnnulusNav E2E tests, ring-model unit/integration tests, ring-filter pass-4 tests, orchestrator pattern tests, technique tests updated to read tuning; add two integration image sidecars.
tests/integration/image_library/images/ring_only_curved/*, tests/nav/nav_technique/test_confidence_config.py, tests/nav/nav_technique/test_nav_technique_ring_annulus.py, tests/nav/nav_model/test_nav_model_rings*.py, tests/nav/nav_model/test_ring_filter.py, tests/nav/nav_orchestrator/test_orchestrator.py, tests/nav/...

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the primary changes: shipping RingAnnulusNav and introducing per-technique configuration management. It accurately reflects the main objectives without unnecessary detail or vague terms.
Docstring Coverage ✅ Passed Docstring coverage is 92.37% 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 thoroughly follows the template structure with all required sections populated: Purpose, Changes, Type of Change, Testing, Potential Impacts, Checklist, and 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 34 minutes and 38 seconds.

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 85.07463% with 50 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (rf_core_rewrite@0d09483). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/nav/nav_technique/confidence_config.py 77.55% 12 Missing and 10 partials ⚠️
...rc/nav/nav_technique/nav_technique_ring_annulus.py 91.83% 4 Missing and 4 partials ⚠️
src/nav/dataset/dataset_pds3.py 0.00% 3 Missing and 3 partials ⚠️
src/nav/config/config.py 69.23% 2 Missing and 2 partials ⚠️
src/nav/nav_model/nav_model_body.py 0.00% 4 Missing ⚠️
src/nav/nav_model/rings/ring_filter.py 72.72% 1 Missing and 2 partials ⚠️
src/nav/nav_model/nav_model_rings.py 94.59% 1 Missing and 1 partial ⚠️
src/nav/navigate_image_files.py 94.73% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                Coverage Diff                 @@
##             rf_core_rewrite     #117   +/-   ##
==================================================
  Coverage                   ?   52.36%           
==================================================
  Files                      ?      131           
  Lines                      ?    15825           
  Branches                   ?     2161           
==================================================
  Hits                       ?     8286           
  Misses                     ?     6977           
  Partials                   ?      562           

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

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

Caution

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

⚠️ Outside diff range comments (3)
src/nav/nav_technique/nav_technique_body_limb.py (1)

238-243: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

BodyLimbNav still uses AT_EDGE_TOLERANCE_PX from dt_fitting module.

Unlike RingEdgeNav which reads at_edge_tolerance_px from its tuning config, BodyLimbNav still uses the imported AT_EDGE_TOLERANCE_PX constant from dt_fitting at lines 239-242. This creates an inconsistency: RingEdgeNav has a configurable at-edge tolerance while BodyLimbNav and BodyTerminatorNav do not.

If this is intentional (shared DT-fitting tolerance vs. technique-specific), consider documenting why. Otherwise, consider adding at_edge_tolerance_px to the BodyLimbNav.tuning block for consistency.

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

In `@src/nav/nav_technique/nav_technique_body_limb.py` around lines 238 - 243,
BodyLimbNav currently hardcodes AT_EDGE_TOLERANCE_PX from dt_fitting when
computing the at_edge boolean; replace that with a tuning parameter so it
matches RingEdgeNav (or document intention). Add an at_edge_tolerance_px entry
to BodyLimbNav.tuning and read it where at_edge is computed (use
self.tuning.at_edge_tolerance_px instead of AT_EDGE_TOLERANCE_PX), then use that
value in the abs(dv_final +/- margin_v) and abs(du_final +/- margin_u)
comparisons; also mirror the same change or add a note/update in
BodyTerminatorNav if it should share the same behavior.
src/nav/nav_technique/nav_technique_body_terminator.py (1)

208-213: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast in navigate() when no eligible terminator features remain after filtering.

navigate() currently proceeds even if eligible_features is empty, which can push empty arrays
into downstream fitting code and produce opaque runtime failures. Raise a clear ValueError here.

Suggested patch
             eligible_features = [
                 f
                 for f in features
                 if isinstance(f.geometry, TerminatorPolyline)
                 and f.geometry.vertices_vu.shape[0] >= self._min_arc_px
             ]
+            if len(eligible_features) == 0:
+                raise ValueError(
+                    'BodyTerminatorNav.navigate() requires at least one eligible TERMINATOR_ARC '
+                    f'with >= {self._min_arc_px:g} vertices'
+                )
             self.logger.info(
                 'Consuming %d TERMINATOR_ARC features (out of %d offered)',
                 len(eligible_features),
                 len(features),
             )

As per coding guidelines: "Validate inputs at the public API boundary of the library. Raise clear
ValueError or TypeError exceptions for invalid arguments."

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

In `@src/nav/nav_technique/nav_technique_body_terminator.py` around lines 208 -
213, In navigate(), after building eligible_features (the list comprehension
that filters features by isinstance(f.geometry, TerminatorPolyline) and
f.geometry.vertices_vu.shape[0] >= self._min_arc_px), add a guard that checks if
eligible_features is empty and immediately raises a ValueError with a clear
message (e.g., "no eligible terminator features after filtering by
TerminatorPolyline and min arc pixels"); this prevents empty arrays from being
passed into downstream fitting code and surfaces a clear API error at the public
boundary.
AUTONAV_PLAN.md (1)

3059-3238: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use single-space sentence separation in the updated Phase 6 section.

Several changed lines use two spaces after a period (for example Line 3059 and Line 3188). Please normalize to one space for consistency and guideline compliance.

As per coding guidelines, "Always use one space between the period at the end of a sentence and the next sentence in documentation".

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

In `@AUTONAV_PLAN.md` around lines 3059 - 3238, The Phase 6 documentation contains
sentences with double spaces after periods; find the updated "Phase 6" narrative
(the "What shipped in Phase 6" section and the "Phase 6 follow-ups" paragraphs
that mention RingAnnulusNav, config_510_techniques.yaml, and
PHASE6_LIBRARY_SEED.md) and normalize sentence spacing to a single space after
each period throughout that section; search for occurrences of "  " (two spaces)
following a period and replace them with a single space, verifying examples near
mentions of RingAnnulusNav and the follow-ups are corrected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@AUTONAV_PLAN.md`:
- Around line 3160-3174: The test count in AUTONAV_PLAN.md is inconsistent: the
summary claims the PR adds 20 tests but the detailed bullet lists show 9 tests
in tests/nav/nav_technique/test_nav_technique_ring_annulus.py and 13 in
tests/nav/nav_technique/test_confidence_config.py (total 22); update the
sentence that asserts "20 tests" to the correct total (22) or add a
parenthetical note clarifying that the documented 13 tests in
test_confidence_config.py represent a subset of the PR's additions—modify the
summary line that references the PR's declared additions and/or the sentence
mentioning test_confidence_config.py to make the counts consistent with the
listed files.

In `@PHASE6_LIBRARY_SEED.md`:
- Around line 267-269: Update the CI description: change the sentence that
claims "CI runs the `integration` mark on every PR" to reflect the current
policy that integration tests are operator/local by default and are excluded
from PR CI (we run pytest with -m 'not integration'); mention the `integration`
mark and the use of -m 'not integration' to make the behavior explicit.
- Around line 42-50: The fenced code block starting with "radial_extent_px = max
- min projection of the polyline onto its mean radial normal" is missing a
language tag; update that markdown block in PHASE6_LIBRARY_SEED.md by adding an
explicit language identifier (e.g., "text") after the opening backticks so it
reads "```text" to satisfy markdownlint MD040 and preserve the existing block
contents and closing backticks.

In `@src/nav/nav_model/rings/ring_filter.py`:
- Around line 217-233: The current restore logic may append outermost_feature
into result even when a feature with the same key already exists (possibly
trimmed), causing duplicates; modify the block around
survives_outermost/outermost_feature so that before appending you check result
for an existing feature with the same key (use outermost_feature.key) and
replace that entry (preserving position if needed) instead of blindly
result.append(...). Update the logic that computes survives_outermost and then
perform a key-based lookup/replace in result (using feature.key from items in
result and feat.all_base_radii()) to ensure only one entry per key.

In `@src/nav/nav_technique/nav_technique_body_terminator.py`:
- Around line 149-153: After reading config in the constructor
(self.config.read_config()) and parsing tuning entries into self._min_arc_px,
self._spurious_dt_rms_factor, self._spurious_dt_floor_px, and
self._spurious_min_inliers, add explicit validation that raises ValueError with
clear messages: verify each numeric is finite (use math.isfinite) and that
_min_arc_px > 0, _spurious_dt_rms_factor >= 0, _spurious_dt_floor_px >= 0, and
_spurious_min_inliers >= 0 and an integer; if any check fails raise ValueError
identifying the offending field and expected bounds so invalid config values
fail fast during initialization.

In `@src/nav/nav_technique/nav_technique_ring_annulus.py`:
- Around line 152-160: The navigate() flow does not guard against empty annulus
templates and should raise a clear error: after computing eligible =
_filter_annulus_features(features) (and before calling compose_template_features
or using template_img/template_mask), check if eligible is empty and raise a
ValueError with a descriptive message (e.g., "No usable RING_ANNULUS templates
available for navigation"); this prevents downstream failures in
compose_template_features and correlation logic and makes the public API
boundary validation explicit.
- Around line 229-235: The _upsample_factor method must validate the external
config value correlation_fft_upsample_factor from self.config.offset: ensure the
raw value exists, is a real number but not a bool, and when converted to int is
>=1 and below a reasonable upper bound (e.g. 1_000_000 or sys.maxsize/2) to
avoid overflow; on any invalid type or out-of-range value raise a clear
ValueError mentioning "correlation_fft_upsample_factor" and the offending value.
Locate method _upsample_factor, check getattr(self.config, 'offset', None) and
then inspect the raw value via getattr(offset_block,
'correlation_fft_upsample_factor', None), validate type with isinstance(...,
numbers.Real) and not isinstance(..., bool), convert to int safely, enforce
bounds, and raise ValueError with a descriptive message if validation fails.
- Around line 258-271: The function _search_window_for_obs currently assumes
context.obs.extfov_margin_vu is an indexable pair of non-negative numbers;
validate this before using it: fetch margin = getattr(obs, "extfov_margin_vu",
None), and if not None check that it is a sequence/tuple of length >=2, that
margin[0] and margin[1] are ints or floats, and that both are >= 0; if any check
fails raise a ValueError with a clear message (mention
NavContext.obs.extfov_margin_vu in the text); on success return (int(margin[0]),
int(margin[1])) and keep the existing (32, 32) fallback when margin is None.

In
`@tests/integration/image_library/images/ring_only_curved/N1447064164_1_CALIB.yaml`:
- Around line 29-36: The sidecar contains TODO placeholders in the Phase-6 note
and uses expected.status: conflicted and expected.confidence_tier: conflicted;
replace the TODO coordinates in the "Phase-6 status: RingAnnulusNav converges to
(TODO, TODO)" line with the actual converged pixel coordinates (from the latest
Phase-6 regression run) and set expected.status and expected.confidence_tier to
the true expected values, or if "conflicted" is intentionally correct, add a
one-line comment explaining why (e.g., "conflicted expected due to known
calibration drift in Phase-6") so the intent is clear for future reviewers;
update the YAML values for expected.status and expected.confidence_tier to match
that decision.

In
`@tests/integration/image_library/images/ring_only_curved/W1444747627_1_CALIB.yaml`:
- Around line 29-32: Update the committed operator-verified sidecar by removing
or replacing the placeholder TODOs in the Phase-6 status sentence: find the line
starting with "Phase-6 status: RingAnnulusNav converges to (TODO, TODO)," and
either insert the measured (x,y) convergence values (and the measured pixel
tolerance instead of "within TODO px") or remove that entire sentence if
measurements are not available; ensure the surrounding confidence/coefficient
text (e.g., "expected.confidence_tier") remains valid and that the file contains
no remaining TODO placeholders.

In `@tests/nav/nav_model/test_ring_filter.py`:
- Around line 503-507: The test currently only asserts membership with "'outer'
in keys" which can hide duplicates or extra survivors; replace that membership
check by asserting the exact expected keys sequence or set (e.g., assert keys ==
['outer'] or assert set(keys) == {'outer'}) depending on whether order matters,
targeting the local variables result and keys in the test function so the test
verifies the precise restored output rather than just existence.

---

Outside diff comments:
In `@AUTONAV_PLAN.md`:
- Around line 3059-3238: The Phase 6 documentation contains sentences with
double spaces after periods; find the updated "Phase 6" narrative (the "What
shipped in Phase 6" section and the "Phase 6 follow-ups" paragraphs that mention
RingAnnulusNav, config_510_techniques.yaml, and PHASE6_LIBRARY_SEED.md) and
normalize sentence spacing to a single space after each period throughout that
section; search for occurrences of "  " (two spaces) following a period and
replace them with a single space, verifying examples near mentions of
RingAnnulusNav and the follow-ups are corrected.

In `@src/nav/nav_technique/nav_technique_body_limb.py`:
- Around line 238-243: BodyLimbNav currently hardcodes AT_EDGE_TOLERANCE_PX from
dt_fitting when computing the at_edge boolean; replace that with a tuning
parameter so it matches RingEdgeNav (or document intention). Add an
at_edge_tolerance_px entry to BodyLimbNav.tuning and read it where at_edge is
computed (use self.tuning.at_edge_tolerance_px instead of AT_EDGE_TOLERANCE_PX),
then use that value in the abs(dv_final +/- margin_v) and abs(du_final +/-
margin_u) comparisons; also mirror the same change or add a note/update in
BodyTerminatorNav if it should share the same behavior.

In `@src/nav/nav_technique/nav_technique_body_terminator.py`:
- Around line 208-213: In navigate(), after building eligible_features (the list
comprehension that filters features by isinstance(f.geometry,
TerminatorPolyline) and f.geometry.vertices_vu.shape[0] >= self._min_arc_px),
add a guard that checks if eligible_features is empty and immediately raises a
ValueError with a clear message (e.g., "no eligible terminator features after
filtering by TerminatorPolyline and min arc pixels"); this prevents empty arrays
from being passed into downstream fitting code and surfaces a clear API error at
the public boundary.
🪄 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: c905b0ec-d2b2-4144-80a9-a4478ce92dd2

📥 Commits

Reviewing files that changed from the base of the PR and between 0d09483 and f3f1987.

📒 Files selected for processing (31)
  • .github/workflows/run-tests.yml
  • AUTONAV_PLAN.md
  • PHASE6_LIBRARY_SEED.md
  • docs/developer_guide_techniques.rst
  • docs/introduction_configuration.rst
  • docs/user_guide_navigation.rst
  • src/nav/config/config.py
  • src/nav/config_files/config_510_techniques.yaml
  • src/nav/nav_model/nav_model_body.py
  • src/nav/nav_model/nav_model_rings.py
  • src/nav/nav_model/rings/ring_filter.py
  • src/nav/nav_orchestrator/orchestrator.py
  • src/nav/nav_technique/__init__.py
  • src/nav/nav_technique/confidence_config.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
  • tests/integration/image_library/images/ring_only_curved/N1447064164_1_CALIB.yaml
  • tests/integration/image_library/images/ring_only_curved/W1444747627_1_CALIB.yaml
  • tests/nav/nav_model/test_nav_model_rings.py
  • tests/nav/nav_model/test_nav_model_rings_integration.py
  • tests/nav/nav_model/test_ring_filter.py
  • tests/nav/nav_orchestrator/test_orchestrator.py
  • tests/nav/nav_technique/test_confidence_config.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_annulus.py

Comment thread AUTONAV_PLAN.md
Comment thread PHASE6_LIBRARY_SEED.md
Comment thread PHASE6_LIBRARY_SEED.md
Comment thread src/nav/nav_model/rings/ring_filter.py Outdated
Comment thread src/nav/nav_technique/nav_technique_body_terminator.py
Comment thread src/nav/nav_technique/nav_technique_ring_annulus.py
Comment thread src/nav/nav_technique/nav_technique_ring_annulus.py
Comment thread tests/nav/nav_model/test_ring_filter.py Outdated
- Composite ring-annulus emission: collapse every annulus-eligible per-ring rendering into a single composite RING_ANNULUS per planet so the reliability formula's constituent_count term scales with the number of fused rings (was constituent_count=1 per feature, gating every annulus out at 0.135 < 0.30 default threshold).
- _composite_ring_renderings helper: union per-ring masks (logical OR) and take the per-pixel max of the rendered images so overlapping ring contributions keep their brightest value.
- Ring filter pass-4 outermost-preservation: replace the trimmed entry by key when the outermost feature is restored, instead of appending a duplicate entry.
- RingAnnulusNav.navigate explicit empty-input validation: raise ValueError with a descriptive message when every input feature lacks a template payload, matching the public API boundary.
- _upsample_factor validation: reject bool, non-numeric, non-finite, and out-of-range values for config.offset.correlation_fft_upsample_factor; bound at 1 .. 1_000_000 to prevent FFT memory blow-ups; module constants _DEFAULT_UPSAMPLE_FACTOR and _MAX_UPSAMPLE_FACTOR document the bounds.
- _search_window_for_obs hard-fails on a missing extfov_margin_vu attribute across all six techniques (was a silent fallback to 32x32).  Test fixtures already supply the attribute via FakeObs; an obs without it is a programming error.
- Adaptive PNG whitepoint: scale the bright-pixel clip count to the 5% of detected outliers (median + 15*MAD threshold) instead of the fixed 0.1% of all pixels.  Dark-sky images with a few bright stars no longer saturate every star to 255; body-fills-FOV scenes keep the original 0.1% behaviour.
- Pytest excludes integration tests by default via addopts = ["-m", "not integration"]; pass -m "" or -m integration to opt back in.
- scripts/run-all-checks.sh gains a -i / --integration flag and prints a one-line message indicating whether integration tests will run.
- Move all per-technique magic constants (LIMB_MIN_ARC_PX, TERMINATOR_MIN_ARC_PX, SPURIOUS_*, _AT_EDGE_TOLERANCE_PX, RING_ANNULUS_MAX_RADIAL_PX, RING_ANNULUS_KMPP_THRESHOLD) out of Python module constants and into config_510_techniques.yaml under per-technique tuning blocks plus the per-planet feature_emission.ring_annulus block.  Tests load values via cls.tuning rather than direct constant import.
- Docs: developer_guide_navigation_models_rings.rst, developer_guide_techniques.rst, user_guide_navigation.rst describe the new per-planet annulus emission gates; CLAUDE.md and CONTRIBUTING.md document the integration-test marker convention.
- Validate `vol_start` and `vol_end` arguments to DataSetPDS3 against the available volume list and raise a descriptive ValueError on a typo.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/run-tests.yml:
- Line 102: The conditional expression in the GitHub Actions job uses
"&&!github.event..." which is valid but hard to read; update the if condition
(the line starting with "if: matrix.os == 'ubuntu-latest' &&
matrix.python-version == '3.13' &&!github.event.pull_request.head.repo.fork") to
add a space after the logical AND so it reads "&&
!github.event.pull_request.head.repo.fork" for improved readability.

In `@src/nav/config/config.py`:
- Around line 204-212: The try/except in config.py swallows
ConfidenceConfigError for underscore-prefixed test techniques and uses continue,
which skips resetting cls.tuning and causes stale tuning to persist; change the
flow so that even when cls.name.startswith('_') you still call
load_technique_tuning (or explicitly set cls.tuning = {} when tuning block is
absent) after handling the ConfidenceConfigError—i.e., remove the continue or
ensure cls.tuning is assigned (via load_technique_tuning or an explicit empty
dict) when the name starts with '_' so tuning is always reset regardless of the
confidence-spec error.
- Around line 202-213: Merged per-technique overrides aren't applied after
update_config because the loader logic that sets NavTechnique subclasses'
confidence_spec and tuning (the loop using NavTechnique._registry with
load_confidence_spec and load_technique_tuning and ending with
validate_registered_confidence_specs()) only runs from read_config(); extract
that block into a reusable helper (e.g. _validate_registered_techniques()) that
iterates NavTechnique._registry, sets cls.confidence_spec via
load_confidence_spec(cls.name), handles ConfidenceConfigError for names starting
with '_' the same way, sets cls.tuning via load_technique_tuning, and calls
validate_registered_confidence_specs(); then call this helper from both
read_config() and update_config() immediately after merging self._config_dict so
updated YAML overrides take effect.

In `@src/nav/dataset/dataset_pds3.py`:
- Around line 570-576: The code currently validates vol_start and vol_end
against all_volume_names and computes vol_start_idx and vol_end_idx but does not
check for the case where both are provided and vol_start comes after vol_end;
add an explicit validation after computing vol_start_idx and vol_end_idx that if
vol_start is not None and vol_end is not None and vol_start_idx > vol_end_idx
then raise ValueError (e.g., "vol_start must not be after vol_end") so callers
get a clear error instead of silently returning no rows; locate this check near
the existing vol_start/vol_end handling that references all_volume_names,
vol_start_idx and vol_end_idx in dataset_pds3.py.

In `@src/nav/nav_model/rings/ring_filter.py`:
- Around line 214-225: The survival check for the outermost feature should match
the exact feature identity and edge label, not just the radius: when unpacking
the chosen triple from after_res_in_range (outermost_radius, edge_label,
outermost_feature), keep edge_label and change survives_outermost to verify that
some feat in result has a base radius/edge pair (r, lbl) equal to
(outermost_radius, edge_label) (i.e., iterate feat.all_base_radii() and match
both r and lbl) or alternatively match feat.key == outermost_feature.key
together with the same radius and edge label so you only consider the exact
edge/feature you selected.

In `@src/nav/nav_technique/nav_technique_ring_annulus.py`:
- Around line 120-122: RingAnnulusNav's constructor currently only calls
super().__init__ and never populates confidence_spec, which causes a cold
RingAnnulusNav().navigate(...) to hit the assert that expects confidence_spec;
fix by initializing confidence_spec during __init__: after
super().__init__(config=config) call the same config-read/populate step other
techniques use (e.g. call self.read_config() or set self.confidence_spec =
self.config.confidence_spec if config already contains it) so confidence_spec is
populated before navigate() is ever invoked; update RingAnnulusNav.__init__
accordingly.

In `@src/nav/navigate_image_files.py`:
- Around line 241-247: The docstring currently claims the bright-outlier
threshold is median + 6 * MAD but the implementation computes median + 15 * MAD;
update the docstring to state the correct threshold (median + 15 * MAD) so it
matches the code path that computes threshold = median + 15 * mad (the
variables/expressions around median, mad, and the 15 * mad multiplier used to
count/clip bright outliers). Ensure the docstring wording mirrors the
implementation (and tests) exactly or alternatively change the multiplier in the
implementation to 6 if you intend to keep the docstring — pick one consistent
value and make both the code (threshold calculation) and the explanatory
docstring match.
- Around line 292-293: The branch in apply_linear_gamma_stretch that currently
does "if white <= black: white = black + 1.0" should use nextafter to preserve
any remaining contrast: replace the +1.0 bump with a call to
math.nextafter(black, math.inf) (or numpy.nextafter(black, float('inf')) if the
project prefers numpy) so white becomes the next representable float above
black; also ensure math (or numpy) is imported in the module. This change
targets the variables white and black inside apply_linear_gamma_stretch and
preserves low-dynamic-range detail safely.

In `@tests/nav/nav_model/test_nav_model_rings_integration.py`:
- Around line 223-232: The test currently only checks for a RING_ANNULUS but
doesn’t verify that RING_EDGE features are suppressed; update the assertions
after calling model.to_features(...) (the features list produced by
model.to_features) to assert the exact expected features—either add assert
len(features) == 1 to ensure only the composite annulus is emitted or explicitly
assert that no feature has feature_type == NavFeatureType.RING_EDGE (e.g., check
that all(f.feature_type != NavFeatureType.RING_EDGE for f in features)); keep
the existing checks for RING_ANNULUS and its flags/mask.

In `@tests/nav/nav_model/test_nav_model_rings.py`:
- Around line 186-212: Update the
test_composite_ring_renderings_unions_masks_and_takes_max_image test so it
includes a true overlapping non-zero pixel between the two inputs and asserts
the per-pixel max behavior: modify img_a and img_b (used in the call to
_composite_ring_renderings) to share at least one coordinate with different
intensities (e.g., img_a higher or img_b higher), then add an assertion that
composite_img at that shared coordinate equals the brighter input value (using
pytest.approx) to ensure np.maximum-style behavior is enforced rather than
last-writer-wins.
🪄 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: 5e255875-46f8-470c-be2e-e39a46d0545a

📥 Commits

Reviewing files that changed from the base of the PR and between f3f1987 and 5d90df5.

📒 Files selected for processing (24)
  • .github/workflows/run-tests.yml
  • CONTRIBUTING.md
  • docs/developer_guide_navigation_models_rings.rst
  • docs/developer_guide_techniques.rst
  • docs/user_guide_navigation.rst
  • pyproject.toml
  • scripts/run-all-checks.sh
  • src/nav/config/config.py
  • src/nav/dataset/dataset_pds3.py
  • src/nav/nav_model/nav_model_rings.py
  • src/nav/nav_model/rings/ring_filter.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/navigate_image_files.py
  • tests/integration/image_library/images/ring_only_curved/W1444747627_1_CALIB.yaml
  • tests/nav/nav_model/test_nav_model_rings.py
  • tests/nav/nav_model/test_nav_model_rings_integration.py
  • tests/nav/nav_model/test_ring_filter.py
  • tests/nav/nav_technique/test_nav_technique_ring_annulus.py
  • tests/nav/test_navigate_image_files.py

Comment thread .github/workflows/run-tests.yml
Comment thread src/nav/config/config.py
Comment thread src/nav/config/config.py
Comment thread src/nav/dataset/dataset_pds3.py
Comment thread src/nav/nav_model/rings/ring_filter.py Outdated
Comment thread src/nav/nav_technique/nav_technique_ring_annulus.py
Comment thread src/nav/navigate_image_files.py Outdated
Comment thread src/nav/navigate_image_files.py Outdated
Comment thread tests/nav/nav_model/test_nav_model_rings_integration.py
Comment thread tests/nav/nav_model/test_nav_model_rings.py
Apply eight verified review findings:

- config.update_config: validate registered techniques after merging when
  read_default=True so user YAML technique overrides take effect.
- _validate_registered_techniques: always reset cls.tuning for underscore
  test techniques so a stale value cannot leak across config reloads.
- dataset_pds3: raise ValueError when vol_start follows vol_end instead
  of silently yielding zero rows.
- ring_filter outermost preservation: match feature key plus exact
  (radius, edge_label) instead of radius alone, so two distinct features
  sharing a radius cannot mask a dropped outer edge.
- navigate_image_files quantile stretch: docstring now matches the
  median + 15 * MAD threshold the implementation uses.
- navigate_image_files quantile stretch: use np.nextafter when white
  collapses to black so any LSB-level contrast survives the bump.
- ring annulus integration test: assert exactly one composite feature
  emits, suppressing all RING_EDGE features.
- composite ring rendering test: include an overlapping non-zero pixel
  with different intensities and assert the brighter input wins.
@rfrenchseti rfrenchseti merged commit c38a2b3 into rf_core_rewrite Apr 30, 2026
7 of 8 checks passed
@rfrenchseti rfrenchseti deleted the core_rewrite_phase6 branch April 30, 2026 14:41
@coderabbitai coderabbitai Bot mentioned this pull request May 7, 2026
5 tasks
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