Skip to content

feat(sqm): self-calibrating SQM — raw-green photometry, per-frame black level, B−V color term, wing correction#532

Merged
brickbots merged 31 commits into
brickbots:mainfrom
mrosseel:sqm-fix
Jul 18, 2026
Merged

feat(sqm): self-calibrating SQM — raw-green photometry, per-frame black level, B−V color term, wing correction#532
brickbots merged 31 commits into
brickbots:mainfrom
mrosseel:sqm-fix

Conversation

@mrosseel

@mrosseel mrosseel commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

SQM wandered 0.7–2.4 mag within a night as auto-exposure moved, and shifted with focus and sensor swaps. Three root causes: a static pedestal constant that never matches the true black level, photometry on the clipped/8-bit processed image, and catalog V magnitudes fed to sensors whose passband isn't V.

Fix (self-calibrating; plate solving untouched)

  1. Raw-green photometry — SQM photometry moves to the linear raw frame (green channel for Bayer); solving stays on the processed image. Cycle is skipped if raw is unavailable.
  2. Per-frame black levelPedestalEstimator joint-fits bg = P0 + rate·exposure over the auto-exposure stream; the intercept is the true black level, the dark-current slope is discarded.
  3. Color termV − T·(B−V) per matched star (Hipparcos B−V by HIP id). Per-sensor T: 0.8 measured for bare imx462/imx290, 0.0 for HQ (IR-cut), imx296 unmeasured placeholder.
  4. Wing correction — the 5px aperture misses 25–38% of star flux in the lens-halo wings, biasing SQM 0.3–0.5 mag bright. WingEstimator measures the enclosed fraction via ring-profile slope cutoff on bright stars, smooths it in a rolling window, and applies −2.5·log10(f) to mzero.
    ==> during the night, atmospheric conditions, focus and lens dewing can 'enlarge' the halo around a star, this method measures this halo on the 'good' images, smooths this measured radius over 20 images and then applies this radius to get better photometric results.

Rejected alternatives (per-sensor zero-point constant, per-frame adaptive apertures, low-percentile annulus background, IR-cut hardware) documented in docs/adr/0020.

Validation

Six exposure sweeps (3× imx462, 3× HQ), full production code path:

old pipeline this PR
within-sweep swing vs exposure 0.7–2.4 mag flat (std 0.07–0.22)
focus sensitivity (same night, refocused) large 0.03 mag
vs hand-held reference meter ±0.85, sign flips within ±0.07 on 4/6 nights, no tuned constants

Two nights read ~0.6 mag from the reference for reasons star photometry can't see (reference pointing/haze suspected); absolute anchoring needs a side-by-side campaign.

Notes

  • Published SQM values change scale vs prior firmware; old logs not comparable.
  • New glossary terms in docs/ax/sqm/CONTEXT.md; decision record in docs/adr/0020-sqm-raw-green-photometry-redesign.md.
  • ruff + tests (89) pass; mypy clean on all sqm modules (solver.py needs the tetra3 submodule initialized, as in CI).

🤖 Generated with Claude Code

mrosseel and others added 10 commits July 14, 2026 23:47
…k level, B-V color term, wing correction

SQM previously wandered 0.7-2.4 mag with auto-exposure and shifted with
focus. Redesign makes it self-calibrating; plate solving is untouched.

- Photometry on the raw green channel (linear), solve stays on the
  processed image; skip the cycle if the raw frame is unavailable
- Black level from a rolling joint fit bg = P0 + rate*exposure over the
  auto-exposure stream (PedestalEstimator); dark-current slope discarded
- Color term V - T*(B-V) per matched star (Hipparcos B-V by HIP id);
  per-sensor T: 0.8 bare imx462/imx290, 0.0 HQ (IR-cut), imx296 unmeasured
- Wing correction: the 5px aperture misses 25-38% of star flux in the
  lens-halo wings; WingEstimator measures the enclosed fraction via
  ring-profile slope cutoff, smooths it, applies -2.5*log10(f) to mzero
- Remove dead noise-floor wiring from the SQM path: the
  include_noise_floor_details block never ran, its output key never
  matched what the solver read, and the estimator was rebuilt per call.
  shared_state.noise_floor() has always held the 10.0 default; the
  camera SNR controller keeps running on that. NoiseFloorEstimator
  stays as a sweep-wizard diagnostic.
- Rejected: per-sensor zero-point constant, per-frame adaptive apertures,
  low-percentile annulus background (all re-introduce exposure or focus
  dependence; see docs/adr/0020)

Validation on 6 exposure sweeps (imx462 + HQ): exposure-flat and
focus-flat (std 0.07-0.22), within +/-0.07 mag of the reference meter on
4 of 6 nights with no tuned constants.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
… clamp

Flux-weighted mzero hands most of the vote to the 1-2 brightest matched
stars — exactly the ones prone to systematic error. On the 2026-07-12
sweep a single near-saturated red giant (B-V 1.47) dragged mzero 0.5 mag
and produced a 0.65 mag SQM outlier while the median star agreed with
the good nights to 0.03 mag.

- mzero is now the median of per-star zero points
- stars peaking above 70% of full scale are excluded (CMOS response
  bends well before hard clip; 75-90% peaks already read low) — same
  threshold feeds the wing estimator, keeping f and mzero consistent
- B-V lookups clamped to [-0.5, 1.2], the range the linear colour term
  was fitted on

Sweep validation: imx462 night-to-night spread 0.63 -> 0.06 mag across
two focus states and a Milky Way field; within-sweep std improves to
0.05-0.12; exposure slopes stay flat. The remaining offset vs the
reference meter is now a stable per-sensor constant (imx462 +0.43, hq
+0.07) consistent with NIR sky emission in the bare sensor passband -
to be set by a proper calibration campaign, not from three sweeps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
The colour term matches the stars to the sensor passband, so the sky is
measured in that passband too. A bare sensor sees NIR sky emission
(airglow, LED/sodium LP beyond 700nm) that a V-band SQM meter does not,
so its sky reads genuinely brighter. sqm_band_offset converts the final
SQM back to the meter scale: imx462/imx290 +0.43, hq +0.07, imx296 0.0
(unmeasured).

Earlier a similar constant was removed because it papered over 0.6 mag
of variable systematics. With the black level, colour term, wing
correction and robust mzero in place the residual is now genuinely
constant (night-to-night spread 0.06 mag across focus states and star
fields), so a per-sensor value is justified. Values remain provisional
(3 sweeps imx462, 2 trusted sweeps hq); refine with a multi-night
side-by-side campaign against a reference meter aimed at the camera
field.

Sweep validation with the offset applied: imx462 -0.03/+0.03/+0.01 vs
meter; hq +0.03/-0.03 on trusted nights.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
The raw-green SQM pedestal used a placeholder bias_offset of 50 ADU
(# TODO: measure), ~188 ADU below the true black level, so on-sky SQM
read ~2.5-3 mag too bright (e.g. 15.5 instead of ~18.6).

Raw-green black level measured three independent ways, all ~238:
- dark-frame calibration (CAL menu, capped): 238.0
- on-sky exposure-sweep intercept (PedestalEstimator P0): 238.6
- rolling estimator fit on the live stream: 238.8

Black level is a per-sensor-model constant (fixed gain 30, libcamera
pipeline), so this ships correct to every unit. imx290 shares the
sensor/optics.
The per-frame PedestalEstimator supplied a pedestal_override that
shadowed the profile/calibration bias_offset. Now that the black level
is a measured per-model constant (238 ADU, shipped in the profile and
refinable per-unit via the CAL wizard), the estimator only adds risk:
under live steady auto-exposure it never gets the >=2x exposure span it
needs, and the one time it does condition (a transient ramp) it can fit
a low intercept and drive SQM too bright. Its slope is sky-dominated so
it can't recover dark current either. Remove the override from the
solver; SQM now always uses the profile/calibration bias_offset.
The solver dispatches align-queue commands by type
(isinstance(command, ReloadSqmCalibration)), and align.py sends typed
objects. The SQM calibration wizard instead enqueued a raw list
['reload_sqm_calibration'], which matched nothing and fell through to
'Unknown solver command' -- so the wizard's reload never fired and new
calibration only took effect on a full restart. Send the typed command.
The raw-green photometry path skipped every cycle on the imx462/imx290:
their cropped green frame (~490px) tripped a hardcoded 512px floor, so SQM
never updated despite good plate-solves. Lower the floor so only missing or
malformed frames are rejected; photometry runs at the green frame's own scale.

The menubar also rendered the 20.15 default as if it were a reading. Only show
a value once one has actually been measured (last_update set).

(ported from the nixos branch)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
A monotonic exposure ramp lets temporal sky drift (dawn twilight,
passing haze) alias into an apparent exposure dependence when a sweep
is analysed — both 2026-07-15 sweeps show it (fake slopes to -0.44
mag/dex from a ~4% sky-rate creep). Random order decorrelates time
from exposure; file names carry the exposure so analysis is unaffected.

Also record the tested-and-rejected faint-star mzero cut in ADR 0020.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
Sweeps carried no record of the constants they were reduced with. Save
the calibration-aware bias_offset, band offset and colour coefficient
(and pass camera_type from the sweep capture path, which never did).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
…nstant

The offset is (sensor passband, fixed) x (sky spectrum, environmental).
Calibrated under LP-dominated suburban sky where it measures constant to
~0.05 mag; expect a different, wandering value under airglow-dominated
dark skies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
mrosseel and others added 3 commits July 15, 2026 19:54
The instrument is stable within a night; its only weak number is the
absolute scale, which depends on the night sky spectrum through the
per-sensor band offset (calibrated for one sky regime). The anchor
measures exactly that unknown: aim the reference meter at the camera
field, enter its reading on the new ANCHOR marking-menu entry of the
SQM screen, and the session is shifted by delta = reference - current.

- UISQMAnchor: meter-value entry (sweep-wizard input pattern), shows
  the live PiFinder value, 0 clears the anchor; delta lives in shared
  state only (resets on restart - tonight's spectrum is tonight's)
- solver applies the delta to sqm_final and sqm_altitude_corrected and
  reports it in details; SQM screen shows "anch +/-x.xx" when active
- every anchor event is appended to ~/PiFinder_data/sqm_anchors.jsonl
  (reference, measured, deltas, camera, location) - accumulating the
  per-site band-offset calibration dataset as a side effect of use
- marking menu reworked: ANCHOR (everyday action), CAL (noise
  constants), SWEEP (diagnostic; label was CORRECT which now belongs
  to the anchor)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
The archived imx296 sweep (100 frames, ref 17.8-17.9) replaces three
placeholders with measurements:

- bias_offset 32 -> 60: the on-sky exposure-ramp intercept is 60.3,
  matching the Sony-standard black level (240 @ 12-bit = 60 @ 10-bit,
  same convention as the imx462 at 238). The old 32 was a
  mis-measurement and biased the background subtraction.
- color_coefficient 0 -> +0.21 (460 stars, mzero vs B-V). Small because
  the Pregius mono QE falls through the NIR, unlike the STARVIS colour
  sensors.
- sqm_band_offset 0 -> -0.46, PROVISIONAL: one moonlit sweep, one hand
  reference; the mono band reads darker than the meter. The session
  anchor absorbs the uncertainty until more nights exist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
SQM photometry is raw-only; the 8-bit processed image is for solving
and display. The PNG path was unreachable in production and measured
strictly worse (0.6-2.4 mag within-sweep swings, errors to 1.7 mag,
frames failing photometry outright) at identical CPU cost.

- solver: create_sqm_calculator always builds the raw calculator;
  update_sqm loses image_processed/use_raw_green and the processed
  fallback branch
- camera_profiles: drop the four _processed profiles and the
  sqm_use_raw_green flag
- CAL wizard: captures, analyzes, verifies and saves the RAW
  calibration only; verification runs the raw pipeline on the raw sky
  frames (green extraction + centroid rescale + 70% saturation cut)
- SQM screen CAL indicator keys off the raw calibration file
- sweep metadata records the raw camera type
- SNR auto-exposure uses the controller defaults (its
  from_camera_profile existed only to consume _processed profiles;
  8-bit defaults 15/30/100 are equivalent)
- noise-floor tests run on a synthetic registered profile instead of
  imx296_processed

Sweep regression after the deletion is bit-identical
(18.275/std 0.070 on the 2026-07-11 reference sweep).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
… module

Surface the reference-meter session feature as "SQM Correct" (matching the
CORRECT marking-menu action). Renames UISQMAnchor->UISQMCorrect
(sqm_anchor.py->sqm_correct.py), shared-state sqm_anchor_delta->
sqm_correct_delta, solver._apply_sqm_anchor->_apply_sqm_correct, and the
per-event log sqm_anchors.jsonl->sqm_corrects.jsonl.

Delete the orphaned UISQMCorrection (sqm_correction.py): upstream removed its
menu launcher in brickbots#374 (2026-03), leaving it unreachable dead code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
mrosseel added 5 commits July 16, 2026 00:49
…tadata

- sqm.py: dark_current_rate/read_noise default to 0.0 so a None (from an
  incomplete calibration file) can't crash the whole SQM calculation on a
  diagnostic-only noise term.
- sqm_sweep.py: always write the sqm/reference block and guard each optional
  enrichment section independently so one failing step no longer discards the
  entire detailed-metadata record.
The solver built the calculator eagerly at startup from
shared_state.camera_type(), which still holds the 'imx296' default until the
camera process finishes libcamera init and reports the real sensor. The
calculator then kept the wrong profile for the whole session, so photometry
subtracted the imx296 pedestal (bias_offset 60) instead of the real sensor's
(238 imx462 / 256 hq), inflating the sky background by ~2-3 mag: devices read
SQM ~15-16 under an ~18 sky (measured: imx296 profile 14.67 vs hq 18.12 on
the same frame).

Create the calculator on first use instead. The camera process records its
real type before capturing its first frame, and a solve requires a captured
frame, so first use is guaranteed to see the real camera type. The
ReloadSqmCalibration command now just invalidates the calculator so the next
solve rebuilds it with fresh calibration, leaving a single creation site.
Offline analysis of every referenced HQ sweep (2025-11-16 x2, 2025-11-18,
2026-07-16; full pipeline incl. per-sweep wing correction) shows a stable
-0.32 mag residual vs the hand-held reference meter (median of -0.28, -0.08,
-0.46, -0.36; spread 0.14). Raise the band offset 0.07 -> 0.39 to absorb it.

Method validated on imx462: the same offline pipeline reproduces on-device
SQM exactly and leaves imx462 residuals at +0.02 +/- 0.04 with its existing
+0.43 offset. Analysis artifacts: myPiFinder/support/dumps/analysis/20260716.
…fsets

When a star's wings sit below the ring noise, the wing-boundary search cuts
at the first ring — at worst the aperture itself, where total==core makes the
enclosed fraction exactly 1.0 regardless of the optics. Faint fields then
fill the rolling window with these tautological 'perfect aperture' samples
and the wing correction collapses (measured: same-night HQ sweeps at f 0.92
vs 0.71, a 0.27 mag SQM split; 20% of the faint sweep's samples were exactly
degenerate).

Require the boundary to sit at least min_wing_radius out: a closer boundary
means the wings were not resolved for that star — a statement about the
star's brightness, not the optics — so the sample is dropped instead of
recorded as f~1. The threshold is one angular cut (~530 arcsec) expressed in
each sensor's photometry-image pixels (CameraProfile.wing_min_radius_px:
7 imx462/imx290 @490px green, 11 hq @760px, 15 imx296 @1088px mono), wired
in the solver when the SQM calculator is created.

Validated offline against all 14 archived sweeps (local cedar pipeline that
reproduces on-device SQM exactly): the same-night HQ pair converges from
f 0.92/0.71 to 0.64/0.67, and per-sweep f spreads tighten on both sensors.
The floor removes samples that previously diluted the correction, so wcorr
rises and both band offsets are recalibrated with it (they absorb the
estimator's mean and must move as a package):

- imx462/imx290: 0.43 -> 0.31 (6 referenced sweeps, residuals +/-0.10)
- hq:            0.39 -> 0.32 (3 independent references; the shared
  2025-11-16 reading sits +0.3 off on both its sweeps — suspect reference)

Analysis artifacts: myPiFinder/support/dumps/analysis/20260716.
…ection; centroid-excluded annuli

Three defects found live on-sky (mr2, 2026-07-17), each verified against the
raw frames before and after:

1. ROTATION (the 3-magnitude bug): the camera process rotates the solve/
   display image (90 or 270 deg CCW depending on screen_direction /
   camera_rotation) but stores the raw frame unrotated. The raw-photometry
   SQM path mapped solve-image centroids straight onto the raw, so every
   star aperture landed on the wrong sky and SQM read ~15-16 under an ~18
   sky on every device with a rotated display (which is all of them).
   The camera now publishes its rotation (SharedState.solve_image_rotation)
   and update_sqm counter-rotates matched + detected centroids onto the raw
   grid (_derotate_centroids, validated against PIL rotation for 0/90/180/
   270 and arbitrary angles). Measured live: matched-star flux recovered
   ~4x; SQM 15.4 -> 17.6 agreeing with the offline pipeline to 0.05.

2. Wing correction was an artifact: the stacked curve of growth of 21
   bright live stars shows the imx462 PSF fully enclosed by r~4 (FWHM
   2.6px) -- no wings. The per-star boundary search integrated noise and
   reported f~0.72-0.75, a spurious +0.3-0.5 mag correction whose variance
   polluted the calibration. WingEstimator now median-stacks
   aperture-normalized bright stars per frame and reads the enclosed
   fraction off the stack's growth-curve plateau: wingless optics measure
   f=1.0 (correction 0); hq's mild focus-dependent wings (f 0.87-1.0)
   are measured honestly. min_wing_radius and
   CameraProfile.wing_min_radius_px are gone.

3. Background annuli in dense fields: every *detected* centroid (cedar
   finds them all; the parameter was previously unused) is now masked out
   of each star's background annulus, with a 3-sigma clip as backstop for
   undetected sources. A neighbour star can no longer inflate the local
   sky estimate.

Band offsets recalibrated against all referenced clear-night sweeps with
this pipeline: imx462/imx290 0.64 (residuals +/-0.06 over 6 sweeps), hq
0.63 (3 independent references). Cloudy-night sweeps excluded and
documented: star-calibrated SQM under bright clouds reads ~0.7-0.9 low vs
a radiometric meter because the zero point is measured through cloud
extinction while the glow largely is not.

Analysis artifacts: myPiFinder/support/dumps/analysis/20260716.
mrosseel added 4 commits July 17, 2026 09:32
The mono sweep (2025-10-31) rerun through the final pipeline (growth-curve
aperture correction: f=0.985, wingless like its siblings; centroid-excluded
annuli) computes 17.45 against the recorded 17.8-17.9 hand-held reference.
Refit: -0.46 -> -0.06. The +0.40 shift matches the phantom-wing retirement
on imx462/hq (+0.33/+0.31), and a near-zero offset is physically consistent
with the Pregius mono passband being the closest of the three sensors to the
meter's. Single moonlit reference night: confidence +/-0.2; re-anchor with
Session Correct or a reference sweep when this camera flies again.
…mp logging

The titlebar SQM runs under PID auto-exposure (10 ms - 1 s), and the sweep
ramps showed the frame zero point drifting ~-0.2 mag/decade of exposure.
Per-star tracking across the ramps (860 star tracks) exonerates the sensor:
response is linear to ±1% below 0.2 full-scale, no compression. The drift is
population composition: matched-star sets slide ~4 magnitudes across the
exposure range, and per-star zero points carry a mild magnitude dependence,
so every exposure-dependent selection (all-star median, top-K, brighter
half) inherits drift.

Three changes:

- mzero is now the median over stars in a fixed catalog-magnitude band
  (3.5-6.5, fallback to all stars below 5 in band): the same physical stars
  vote at every exposure. Measured drift on the imx462 ramps:
  -0.10 -> -0.01 mag/decade. hq keeps a ~-0.15 residual consistent with its
  real focus-dependent wings (tracked per session by the growth estimator).

- Background annulus moved 6-14 -> 10-18, outside the PSF-wing zone that
  inflated bright stars' local sky at long exposure (~35% of the drift);
  safe against neighbours now that all detected centroids are masked out
  of annuli.

- Sensor die temperature (picamera2 SensorTemperature) recorded into
  last_image_metadata and sweep metadata, diagnostic only: the residual
  black level wanders ±2 ADU night-to-night and temperature is the prime
  suspect; normal sweeps now collect the correlation data.

Band offsets refit against all referenced clear-night sweeps with this
pipeline (they absorb the annulus/selection level shifts):
imx462/imx290 0.61, hq 0.60, imx296 -0.10. Clear-night residuals:
imx462 ±0.08 (6 refs), hq -0.16..+0.18 (3 refs, the shared 2025-11-16
reading remains the outlier). Cloud bias unchanged (-0.4..-0.6, physics).
…tion

Publish SQM as a fixed-calibration radiometer (factory zero point x measured
sky flux) at 1 Hz from every camera frame, no plate solve required. Stellar
photometry becomes a 10-second transmission diagnostic that establishes,
maintains and audits the calibration:

- sqm/radiometer.py: camera-side sparse-central-median sample; 12-sample/15 s
  accumulator; factory radiometric zero point + FOV per sensor profile.
- sqm/clouds.py: cloud/dew discrimination. Cloud dims stars AND brightens a
  light-polluted sky (radiometer immune, no correction); dew/optics dim stars
  and sky together (stellar deficit compensates the radiometer once a clear
  session baseline exists). Asymmetric median baselines, factory-seeded.
- sqm/black_level.py: sensor pedestal tracked from the sky-vs-exposure
  intercept of ordinary frames (the true black level wanders +/-2 ADU night
  to night); fed at 1 Hz, gated on transmission stability, zero-touch only.
- sqm/gaia_ref.py + astro_data/hip_gaia_g.npz: Gaia G reference band with
  BP-RP trim for bare sensors (24-29% less star scatter than Hipparcos V).
- Robust stellar zero point: fixed catalog-magnitude band + 3-MAD rejection;
  centroid-excluded background annuli; growth-curve aperture correction.
- Calibration wizard rework: temporal read noise, multi-exposure dark fit,
  exact frame/solve pairing, typed reload command, transient exposures.

Archive replay (16 sweeps, 800 frames): imx462 MAE 0.05 mag over six
SQM-L-referenced clear nights; bright-cloud errors 0.35-0.84 collapse to
under 0.10 with zero corrections on clear skies; 800/800 frames published
including 318 without usable stars.
mrosseel added 2 commits July 18, 2026 00:50
Each sweep image pair now gets img_NNN_XXms_metadata.json carrying the
driver-applied exposure/gain, sensor die temperature, sensor black levels,
Lux, colour gains, and raw-frame ADU statistics (mean/median/std/percentiles,
saturated fraction). frame_metadata.json aggregates the same records.

Also gate the sweep UI COMPLETE transition on sweep_metadata.json existing:
the camera process writes it a few seconds after the last image, and
enriching before that silently skipped the SQM/solve/noise-floor details
on every sweep.
Each tracker (BlackLevelTracker, CloudEstimator, WingEstimator,
RadiometerAccumulator) gains dump(): config, derived stats, and the full
window sample contents. The solver publishes these into sqm_details as
window_black_level / window_clouds / window_wings / window_radiometer on
every update, and each sweep image JSON snapshots the whole sqm_details,
so a sweep records the pipeline state frame by frame.
…erization

Plan B for the stuck-pedestal incident (2026-07-17):
- BlackLevelTracker: an accepted pedestal is now a lease (max_age_seconds,
  default 900 s) instead of a latch — without a fresh accepting refit it
  expires and callers fall back to the profile constant, the same state
  every session boots in. Regression test replays the incident.
- max_intercept_stderr tightened 1.0 -> 0.6 ADU: honest ramp fits on the
  27-sweep archive run 0.10-0.69; the cloud-poisoned fit passed at 0.97.

Calibration wizard (dark-frame path the imx296 tester will run):
- persists a per-frame report JSON (median/MAD/percentiles, requested AND
  driver-reported exposure, sensor temp) beside the fitted scalars — the
  ramp shape is where short-exposure pedestal elevation shows, and three
  fitted numbers cannot preserve it
- dark-current fit uses driver-reported exposures (the imx477 delivers
  intermittent half-exposure frames; a requested-exposure fit is silently
  corrupted)
- dark ladder extended from one decade to near sensor minimum, covering
  the elevation zone (imx462: +2.9 ADU at 25 ms, 0 at ~300 ms)

Sweep instrumentation:
- per-image records now carry the full driver metadata dict (new sensors
  report different keys) and the live camera-side radiometer sample
  (the sqm_details snapshot freezes while the solver starves mid-sweep)
- camera_pi keeps last_frame_metadata; both image_metadata sites publish
  actual_exposure_us
mrosseel and others added 5 commits July 18, 2026 15:38
…mat stragglers

mypy: the B-V cache globals are Optional, so assignments cannot narrow
them across statements; parse into locals and use those for savez/len.
format: 4 files the CI format session was silently reformatting on every
run (timeentry, three tests).
Telemetry captured only IMU and solve streams — blind to the radiometric
side. A third event type records the camera-side radiometer sample at
~1 Hz: t (capture epoch, s), exp (driver-reported exposure, s), bg
(background median, ADU), mad (median absolute deviation, ADU), grad
(quadrant gradient, ADU), seq (frame sequence). Dedupes on sequence;
replay players skip unknown event types, so old builds read new files.

A full-night radio stream answers what sweep snapshots cannot: how
auto-exposure actually behaves over a session (lever-arm availability
for the black-level fit), a continuous sky-steadiness timeline, and
real-night input for replaying pedestal estimators.
The marking menu down slot is the one readable at the eyepiece without
moving your head — it goes to SWEEP, the everyday reference-meter action.

SQM Correct is deleted rather than kept: a magnitude-additive session
knob silently absorbs ADU-space, brightness-dependent errors (tonight:
a stuck pedestal), masking exactly the fault class it appears to fix,
and staling whenever the layers beneath it change. Its legitimate
output — the unit-vs-reference-meter difference — is already recorded
as evidence by every sweep run with a reference reading. The feature
never shipped upstream (fork-only), so now is the cheapest moment to
remove it.

Removed: UISQMCorrect screen, menu entry and corrected indicator,
_apply_sqm_correct and both solver call sites, the sqm_correct_delta
shared state, and all test references. CONTEXT.md records the decision.
@brickbots
brickbots merged commit b5b1688 into brickbots:main Jul 18, 2026
4 checks passed
brickbots added a commit to mrosseel/PiFinder that referenced this pull request Jul 18, 2026
…lision

Both branches independently added the same raw tetra3 output field to
SuccessfulSolve/PointingEstimate: main's SQM redesign (brickbots#532) as
matched_catID, this branch as matched_catalog_ids. Resolved to main's
matched_catID — it mirrors the raw tetra3 solution key, consistent with
the sibling fields matched_centroids/matched_stars, and is already
consumed by the SQM calibration UI on main. The focus-side consumers
(ui/preview.py, focus tests) now read matched_catID; focus.py's internal
parameter names are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brickbots added a commit that referenced this pull request Jul 18, 2026
…rsion (#542)

PR #532 (SQM redesign) also touched how the solver hands images to
cedar-detect, and those changes broke the shared-memory solve path: the
request now passes reopen_shmem, but the checked-in cedar_detect_pb2
bindings (tetra3 submodule @ 38c3f48f, unchanged by the PR) have no such
field on Image, so every shmem ExtractCentroids call raises
  ValueError: Protocol message Image has no "reopen_shmem" field
before any RPC is made — and since that is not a grpc.RpcError, the
inline-image fallback never engages.

None of these changes were required for the SQM functionality, so this
restores the cedar-detect/tetra3 handoff exactly as it was in the commit
before the merge (b5b1688^):

- drop the _alloc_shmem override that tracked fresh segments
- drop reopen_shmem from the shmem CentroidsRequest Image
- drop the added shmem-fallback warning log
- load the tetra3 pattern database by explicit path again instead of by
  name

The SQM-related solver changes from #532 (raw-green photometry,
radiometer cadence, matched_catID passthrough, lazy calculator creation)
are untouched, as is the pre-existing stale-shmem cleanup.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
brickbots added a commit that referenced this pull request Jul 18, 2026
PR #532 renamed the camera loop's test-mode flag (debug -> test_mode_on)
but moved its initialization inside the capture loop, resetting it to
False every frame, and dropped the command handler that flipped it when
the UI sends "debug" on the camera queue (Tools > Test Mode, and console
key 0). Test mode therefore did nothing: the camera kept publishing real
captures instead of the disk test image.

Hoist the initialization back above the loop and reinstate the "debug"
command toggle. The test-mode branch itself (including #532's
set_sqm_radiometer_sample(None) guard) is unchanged.

Verified headlessly: before activation the Focus preview cycles the
debug camera's three frames (including the starless one); after
Tools > Test Mode it stays pinned to test_images/pifinder_debug_02.png
on both the Focus and Align (Day) screens, and the solver solves it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brickbots added a commit that referenced this pull request Jul 19, 2026
PR #532 renamed the camera loop's test-mode flag (debug -> test_mode_on)
but moved its initialization inside the capture loop, resetting it to
False every frame, and dropped the command handler that flipped it when
the UI sends "debug" on the camera queue (Tools > Test Mode, and console
key 0). Test mode therefore did nothing: the camera kept publishing real
captures instead of the disk test image.

Hoist the initialization back above the loop and reinstate the "debug"
command toggle. The test-mode branch itself (including #532's
set_sqm_radiometer_sample(None) guard) is unchanged.

Verified headlessly: before activation the Focus preview cycles the
debug camera's three frames (including the starless one); after
Tools > Test Mode it stays pinned to test_images/pifinder_debug_02.png
on both the Focus and Align (Day) screens, and the solver solves it.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants