fix(optics): no FOV gate for frames from an unknown optical train - #632
Merged
Conversation
`--camera debug` stopped solving for anyone with a stated lens. ADR 0027 relabelled the debug camera's sensor to `hq` so the archived 10.2 degree frames would be covered, but the lens half still reads live from `camera_lens`, and `resolve_lens` honours a statement whatever profile it is handed. So the gate gets derived from hq x 16mm (17.12 deg) or hq x 12mm (20.43 deg) -- trains that have never physically existed -- and every debug frame falls outside the window. Measured against the real solver: hq + no stated lens 10.33 deg [8.78, 11.88] solves (fitted 10.20) hq + stated 25mm 10.33 deg [8.78, 11.88] solves hq + stated 16mm 17.12 deg [14.55, 19.69] no solve hq + stated 12mm 20.43 deg [17.37, 23.49] no solve Same shape as the regression ADR 0029 exists to fix, mirrored: there an assumption wore a statement's confidence, here a statement is made about hardware that is not in the loop. Names the state rather than special-casing the debug camera. An **unknown optical train** is one whose frames did not come through this device's optics, and it extends the confidence ladder 0029 already established: stated -> +/-15%, assumed -> union over shipped lenses, unknown -> no gate at all. Two consequences follow from one rule -- nothing about the device may be asserted about the frames (no FOV hint), and nothing about the device may be inferred from them (no lens self-heal). The self-heal half is a live bug this records the fix for: with no `camera_lens` in config, debug mode solves at 10.20 deg, self-heal matches that to the hq's 25mm within 1.3%, and writes it into the developer's config. Attach a real imx462 afterwards and the now-stated 25mm derives 6.4 deg, nothing solves, and self-heal cannot undo it -- it only ever writes into an absence. Docs only; the code change follows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUbR3ryYDL7RizsNhPxGkX
`--camera debug` stopped solving for anyone with a stated lens. This is the code half of the decision recorded in the previous commit. The debug camera declares the sensor its archived frames were shot on (`hq`), but the lens half of the train is read live from `camera_lens`, so stating one pairs `hq` with glass that is not in the loop: 16mm implies 17.12 deg and 12mm 20.43, against frames that are 10.2. tetra3 prunes by implied field of view before verification, so every frame is rejected -- no solves at all, and the symptom presents as an exposure problem. Rather than special-case the debug camera, name the state and let the two consumers that must not act on it read one flag: * `CameraInterface.optical_train_known()` defaults True, so a camera has to opt *out* and a new hardware backend inherits the gate rather than silently losing it. `CameraDebug` returns False. * The camera process publishes it beside `camera_type`; `SharedStateObj` defaults it True so the boot window before the camera reports keeps its gate on real hardware. * The solver omits `fov_estimate`/`fov_max_error` entirely -- not a third window to keep consistent, and any frame from any train solves, which is what lets a developer drop their own captures into test_images/. * Lens self-heal declines to write. This half fixes a bug that has been live since 0029: a debug run on a config with no lens fits 10.20 deg, matches that to the hq's 25mm within 1.3%, and states it. Rich's own log has it firing on 2026-08-17. Attach a real imx462 afterwards and the now-stated 25mm derives 6.4 deg, nothing solves, and self-heal cannot undo it -- it only ever writes into an absence. Verified end to end, not just in unit tests: launched headless with `camera_lens: "16mm"` stated -- the exact config that produced zero solves -- and it solves RA 296.37 Dec -1.70 in Aql, fitted FOV 10.20, 21 matches, `solve_source: CAM`, with the integrator logging that self-heal is off and the config left unwritten. test_optics_solving.py now parametrises over every lens a config can name, asserting both halves: the derived gate rejects these frames for 16mm and 12mm, and no-gate solves them whatever is stated. That gap -- every existing case passed no lens key at all -- is why this shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUbR3ryYDL7RizsNhPxGkX
brickbots
marked this pull request as ready for review
August 19, 2026 22:38
TakKanekoGit
pushed a commit
to TakKanekoGit/PiFinder
that referenced
this pull request
Aug 21, 2026
…ickbots#632) * docs(optics): name the unknown optical train, the FOV gate's third rung `--camera debug` stopped solving for anyone with a stated lens. ADR 0027 relabelled the debug camera's sensor to `hq` so the archived 10.2 degree frames would be covered, but the lens half still reads live from `camera_lens`, and `resolve_lens` honours a statement whatever profile it is handed. So the gate gets derived from hq x 16mm (17.12 deg) or hq x 12mm (20.43 deg) -- trains that have never physically existed -- and every debug frame falls outside the window. Measured against the real solver: hq + no stated lens 10.33 deg [8.78, 11.88] solves (fitted 10.20) hq + stated 25mm 10.33 deg [8.78, 11.88] solves hq + stated 16mm 17.12 deg [14.55, 19.69] no solve hq + stated 12mm 20.43 deg [17.37, 23.49] no solve Same shape as the regression ADR 0029 exists to fix, mirrored: there an assumption wore a statement's confidence, here a statement is made about hardware that is not in the loop. Names the state rather than special-casing the debug camera. An **unknown optical train** is one whose frames did not come through this device's optics, and it extends the confidence ladder 0029 already established: stated -> +/-15%, assumed -> union over shipped lenses, unknown -> no gate at all. Two consequences follow from one rule -- nothing about the device may be asserted about the frames (no FOV hint), and nothing about the device may be inferred from them (no lens self-heal). The self-heal half is a live bug this records the fix for: with no `camera_lens` in config, debug mode solves at 10.20 deg, self-heal matches that to the hq's 25mm within 1.3%, and writes it into the developer's config. Attach a real imx462 afterwards and the now-stated 25mm derives 6.4 deg, nothing solves, and self-heal cannot undo it -- it only ever writes into an absence. Docs only; the code change follows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUbR3ryYDL7RizsNhPxGkX * fix(solver): hand no FOV gate to frames from an unknown optical train `--camera debug` stopped solving for anyone with a stated lens. This is the code half of the decision recorded in the previous commit. The debug camera declares the sensor its archived frames were shot on (`hq`), but the lens half of the train is read live from `camera_lens`, so stating one pairs `hq` with glass that is not in the loop: 16mm implies 17.12 deg and 12mm 20.43, against frames that are 10.2. tetra3 prunes by implied field of view before verification, so every frame is rejected -- no solves at all, and the symptom presents as an exposure problem. Rather than special-case the debug camera, name the state and let the two consumers that must not act on it read one flag: * `CameraInterface.optical_train_known()` defaults True, so a camera has to opt *out* and a new hardware backend inherits the gate rather than silently losing it. `CameraDebug` returns False. * The camera process publishes it beside `camera_type`; `SharedStateObj` defaults it True so the boot window before the camera reports keeps its gate on real hardware. * The solver omits `fov_estimate`/`fov_max_error` entirely -- not a third window to keep consistent, and any frame from any train solves, which is what lets a developer drop their own captures into test_images/. * Lens self-heal declines to write. This half fixes a bug that has been live since 0029: a debug run on a config with no lens fits 10.20 deg, matches that to the hq's 25mm within 1.3%, and states it. Rich's own log has it firing on 2026-08-17. Attach a real imx462 afterwards and the now-stated 25mm derives 6.4 deg, nothing solves, and self-heal cannot undo it -- it only ever writes into an absence. Verified end to end, not just in unit tests: launched headless with `camera_lens: "16mm"` stated -- the exact config that produced zero solves -- and it solves RA 296.37 Dec -1.70 in Aql, fitted FOV 10.20, 21 matches, `solve_source: CAM`, with the integrator logging that self-heal is off and the config left unwritten. test_optics_solving.py now parametrises over every lens a config can name, asserting both halves: the derived gate rejects these frames for 16mm and 12mm, and no-gate solves them whatever is stated. That gap -- every existing case passed no lens key at all -- is why this shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUbR3ryYDL7RizsNhPxGkX --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Grilling session output for "debug mode stopped solving after the 2.6.2/3 lens system". Two commits: the decision (CONTEXT.md + ADR 0029 amendment), then the code.
The bug
--camera debugderives its FOV gate fromhq × whatever lens the config states. ADR 0027 fixed the sensor half (relabelled the debug cameraimx296→hq, PR #609) and left the lens half reading live fromcamera_lens, whichresolve_lenshonours whatever profile it is handed. Measured against the real solver and the shippeddefault_database.npz:25mm16mm12mmSo anyone who has opened the Lens menu — and every device whose lens 0029's self-heal has written — loses debug mode.
tests/test_optics_solving.pymissed it because every case calledbuild_optical_train("hq")with no lens key, exercising only the assumed path.Second bug, live since 0029, fixed here too. With no
camera_lensin config, debug mode solves at 10.20°,identify_lens_from_fitted_fovmatches that to the hq's25mmwithin 1.3%, and the integrator states it. It is in Rich's own log firing on 2026-08-17:Attach a real imx462 afterwards and the now-stated 25 mm derives 6.4°, nothing solves, and self-heal can't undo it — it only ever writes into an absence.
The decision
A new term, unknown optical train: a camera whose frames did not come through this device's optics. It extends 0029's ladder rather than special-casing the debug camera —
stated → ±15% · assumed → union over shipped lenses · unknown → no gate at all
— and one rule covers both bugs: nothing about the device may be asserted about the frames (no FOV hint), and nothing about the device may be inferred from them (no self-heal write).
Rejected, with reasons in the ADR: declaring a lens from the camera (needs a second writer for
camera_lens, and gates out frames you drop in yourself — the main use of debug mode); emulating the configured train (right answer to a different question, wants captures we don't have); a database-range gate (no-hint with extra steps); rejecting non-shipped pairings inresolve_lens(fixes this and the sensor-swap deadlock, but makes a user's statement conditionally non-authoritative — a direct contradiction of 0027, and it deserves its own decision).Cost of no-hint, measured: all three widths return identical RA/Dec,
MatchesandProbon the shipped frames, within ~1 ms. What it gives up is the upper bound that rejected confident mis-solves in 0029's noise trials — a protection that matters to a device under the stars, and nothing is under the stars in debug mode.What changed
CameraInterface.optical_train_known()defaults True, so a camera has to opt out — a new hardware backend inherits the gate rather than silently losing it.CameraDebugreturns False.camera_typeinget_image_loop;SharedStateObjdefaults it True so the boot window before the camera reports keeps its gate on real hardware.solver.pyomitsfov_estimate/fov_max_errorentirely, and says so in the one-shot train log line._warn_if_outside_solver_databaseis skipped — it's only meaningful against a gate we're actually handing over.LensSelfHeal._observereturns early, ahead of the stated-lens branch so a streak counted on real optics can't be spent on a recording.Scope stops there. The frustum keeps deriving from the configured optics — it answers "what would my camera image", a question about the device, and propagating would delete a frustum that is correct on a dev laptop (10.33° vs 10.20° frames). SQM is unaffected: only
camera_pipublishes radiometer samples.Verification
End to end, not just unit tests. Launched headless with
camera_lens: "16mm"stated — the exact config that produced zero solves:with
Optical train is unknown ... lens self-heal is offin the log and the config left unwritten. (A stated 16mm gates [14.55, 19.69], so a solve is only possible via the no-gate branch.)-m "unit or smoke"; 20 passed intest_optics_solving.py-m integrationfailures (test_radec_entry,test_all_ui_modules_covered) reproduce on untouchedorigin/main— the second only whentest_battery_titlebar_icon.py's_BareModulehelper is loaded in the same session. Pre-existing, unrelated.New tests: every lens a config can name, asserting both that the derived gate rejects these frames and that no-gate solves them; the self-heal bar plus a control showing the identical fit would have promoted on real optics; the opt-out default in both directions.
Not in scope
The sensor-swap deadlock — switch imx296 → hq from the Camera Type menu with a stated 16 mm → 17.12°, no solves, no way to measure out. Real hardware, real statement, different failure class. Rich says it's filed; the closest issue I found is #613.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CUbR3ryYDL7RizsNhPxGkX