No-QE-curve color calibration. SSSC solves the camera's
true effective response R(λ) from the stars already in the
image — compared against real measured spectra from the Gaia mission
— rather than taking a sensor QE curve as an input. Filter curves stay
as inputs; everything else (sensor QE at operating temperature, AR coating,
optics, atmosphere) is absorbed into R(λ) and solved
empirically. Each run on the same filter+camera combination refines the
solution and seeds the next.
Three buttons across the top row set up and run the calibration: Step 0 plate-solves the active frame, Step 1 fetches calibrator stars and their spectra, and Step 2 runs the SSSC solver on the measured stellar photometry. The dialog remembers the last filter/sensor selections and the Camera / Rig label — so continuing a long-term calibration on the same equipment is just “open, Step 1, Step 2”.
SSSC needs a plate solution to map pixels to sky coordinates. If the active document already has a WCS it's used directly; otherwise the Step 0 button runs the plate solver in-place and refreshes the WCS. Step 1 will offer to call Step 0 for you if you skip ahead.
Mechanically identical to the SFCC fetch step (same auto-σ SEP waterfall,
same bulk Gaia-library match, same SIMBAD query, same B−V letter inference,
same synthetic Johnson B/V/R via gaiaxpy) — see the SFCC doc
for the plot interpretation. Choose a White Reference SED for diagnostics
(G2V by default; it does not affect the solved R(λ)).
Three rows configure the optical chain:
IMX492 · EdgeHD f/7. Together with the filter
selections this uniquely identifies the imaging train, and separates the
saved R(λ) history per sensor/optical train so that
solutions from different rigs sharing a filter set don't mix. Leave blank
if you only have one setup.The SASP Viewer button opens the same curve explorer used by SFCC (see its documentation for details) — useful for sanity-checking a custom filter curve you just imported with Add Curve….
The main action, with the standard options alongside:
SSSC accumulates calibration quality across runs. Every solve is saved, keyed on the filter + camera combination, into the Gaia XP sqlite cache:
Over several nights of imaging with the same rig, the solution converges to your actual rig-specific R(λ) — a measurement no datasheet could give you. See §2.7 for the convergence endgame.
After Step 2 the lower pane shows four panels; the suptitle names the stage reached, star count, and per-stage RMS waterfall:
The headline plot. For Stage 3 each channel's own solved
R(λ) is drawn as a solid colored line (R red, G green, B blue)
inside its filter passband, with the three filter transmission curves
shaded faintly behind. For Stage 1/2 a single stitched dashed grey
composite is drawn instead, because the stages don't solve for the detailed
shape. If a manufacturer QE curve is passed in (not by default), it's
overlaid in dashed amber for comparison. This panel is the plot that
replaces the QE datasheet: it's what the solver saw, through this
camera, on this night. A healthy Stage 3 result shows three smooth
response lobes, one per channel, each covering its own filter without wild
spikes or dropouts.
Scatter of fractional residuals (meas/exp − 1) in R/G
and B/G ratio space vs expected ratio, four series: dark circles for
before correction (what the raw image showed), lighter triangles for
after. The before clouds should sit off the zero line (that's the
colour error SSSC is fixing); the after triangles should collapse to a
tight band on zero. The horizontal dashed line is the perfect-calibration
reference. If a residual slope or curvature remains, Stage 3 is still
converging — another run on the same rig will tighten it further.
A horizontal bar chart with one row per bootstrap stage, bar length =
fractional-residual RMS, shorter is better. Stages that ran show as
coloured bars with their RMS number; a green ▼ delta
annotation on the right side of a row quantifies the improvement
over the previous stage. Stages that did not run show as faint
placeholder bars with an italic note (“need 142 more stars”
or “Requires DR4 + multi-night airmass data”). The small
monospace block at the bottom right of this panel reports the final gains
k_R, k_G=1, k_B and the B−V span of the calibrator
population — the two numbers that gate stage selection.
Histogram of the B−V colour index of the valid
calibrator stars, with the actual range used marked by two orange dashed
lines. A good Stage 3 field looks like a broad hump covering roughly
-0.3 to +1.8 — hot O/B stars pin the blue
edge of each filter, cool K/M stars pin the red edge, together they
triangulate the shape of R(λ). If the title says
“need N more stars for Stage 3” this panel is where
you'll see whether to point at a denser field (add stars) or a wider
colour spread (add span).
Step 2 commits the calibrated image as an “SSSC Calibrated” step; the metadata records the stage reached, star count, residual RMS, gains, B−V range and session id for traceability. A completion dialog repeats the headline numbers and tells you how many more stars (if any) stand between you and Stage 3 on the next run.
RGB only. The core departure from SFCC: SSSC never
multiplies by a sensor QE curve. Instead it treats the per-channel system
response R_c(λ) as the unknown and solves it from the
measured photometry of stars with known Gaia XP spectra. The filter
transmission is the only published optical datum that enters the solver.
gaiaxpy; the Pickles 1998 stellar
flux library and SIMBAD (CDS Strasbourg) as fallbacks when no XP spectrum is
available; SEP for source extraction. The empirical system-response solver,
the bootstrap-stage machinery, the hat-basis control-point parameterisation,
the directional coordinate descent, the session-cache seeding scheme, and
every SSSC-specific guard below are SetiAstro's own work — see the
published
whitepaper for the formal derivation.For every star i and channel c ∈ {R, G, B}:
where f_star(λ) is the Gaia XP calibrated spectrum
(interpolated onto the 3000–11000 Å, 1 Å grid),
T_filter^c is the published filter transmission (combined
with any LP/cut filters), R(λ) is the unknown
effective system response the solver is after, and k_c is a
per-channel scalar gain. R(λ) absorbs everything the manufacturer
QE curve cannot tell you — true sensor QE at operating temperature,
AR coating transmission, mirror and lens throughput, flattener, and
atmospheric extinction at the time of observation.
The solver automatically picks the richest stage the data supports.
Two numbers decide: N = valid calibrator stars, and
Δ(B−V) = (B−V)_max − (B−V)_min =
colour span of the population:
Every stage is strictly more expressive than the one below it, so Stage 3 must beat Stage 2's RMS to be kept — a strict-improvement guard reverts to Stage 2 if the Stage 3 fit regresses (§2.5).
The baseline. Compute measured and expected band ratios
meas_RG = R_meas/G_meas, exp_RG = S_R/S_G
(and likewise for B/G); the scalar gain that minimises per-star residual
is the median ratio of ratios:
with k_G = 1 (G is the reference), each gain clipped to
[0.1, 10]. Equivalent to a traditional scalar SPCC white
balance — but computed with no QE curve.
Same three-model fit as SFCC §2.4 (slope-only closed form, affine
least-squares, quadratic polyfit), scored by fractional-residual RMS and
selected per channel. The important difference is that the expected integrals
S_c are now computed with filter × LP only
— no QE curve enters anywhere.
Two SSSC-specific guards protect against the pathological-field blow-ups that can occur with very wide B−V populations (carbon stars, extreme mismatches):
|a_q| < 10^6). If any check fails, quadratic is rejected
and the slope/affine winner stands.10^6 or is non-finite,
SSSC reverts to the Stage-1 scalar RMS so the reported number stays
meaningful rather than reporting a bogus explosion.The main SSSC contribution. For each channel independently SSSC solves
for a piecewise-linear R_c(λ) parameterised by
N_ctrl control points inside the filter's passband:
where Λ_j are tent (hat) basis functions on the
ctrl_wl grid. Precomputed per-star weight matrices turn the
spectral integrals into one matrix-vector product per channel:
Control points cover the wavelength range where
T_filter^c is above a per-channel fraction of its peak:
| Channel | Threshold | Rationale |
|---|---|---|
| R | 0.30 |
Tight enough to exclude cross-channel leakage, loose enough to cover the steep astro-filter rolloffs. |
| G | 0.707 (half-power) |
OSC Bayer green filters have a secondary red lobe that would otherwise contaminate G integrals. The half-power clip removes it. Also applied to the G integrals used in Stage 1/2 ratio math so the baseline gains don't inherit the leak either. |
| B | 0.30 | Same as R. |
The loss is the sum of squared fractional residuals in colour-ratio space — the same space Stage 1 and Stage 2 are evaluated in, so all three RMS numbers are directly comparable in the waterfall plot:
A directional coordinate descent optimises
r_j^(c): for each control point, probe UP first (×1.1
then ×1.2) and take the best improving step; only if neither
helps, probe DOWN (×0.9, ×0.8); else stay:
Up to 60 sweeps; converges when no point moved on a full sweep or the
loss change drops below 10^-4. The up-first ordering makes
refinement sweeps (seeded from a prior solve) cheap — a point already
near its optimum typically settles in one or two probes.
When the session cache (§2.6) has a prior Stage-3 solve for this
session_id, the solver seeds x_0 from that solve's
per-channel control points, interpolated onto the current ctrl grid:
Three checks gate the seed:
ctrl_points stored (older cache entries
with only the stitched composite are rejected — the composite is
a filter-weighted average across channels and maps poorly back to
individual control points, producing runaway RMS).N_ctrl must match exactly — the W-matrix hat-function
scale depends on hat width, which depends on N_ctrl.
An 8-pt prior into a 16-pt solver has a ∼2× scale mismatch.2.0.x_0's loss must be no more than 5×
the flat Stage-2 operating-point loss. If a prior is incompatible for
any other reason, this catches it and starts fresh instead.Stage 3 is strictly more expressive than Stage 2, so it must match or beat it on RMS. If it doesn't — bad seed, pathological field, or any other reason:
and the rejected ctrl_points are not written to the cache (so a bad solve can't poison the next session's seed). SSSC never regresses.
Even for a Stage 3 solve, the per-pixel correction applied to the
image uses the Stage 2 quadratic coefficients — the same
pivot-at-median application as SFCC. Stage 3's full
R(λ) drives the diagnostics panel and seeds the next
session; it does not go directly to pixels. This is deliberate: the
piecewise-linear R(λ) on 8–16 control points can
have mild oscillation that is harmless in integral space (the diagnostic)
but would be loud when evaluated per pixel. The Stage 2 quadratic is
well-conditioned and inherits all the gain information that matters for
colour.
Each solve is tagged with an 8-character session id derived from the filter+camera configuration:
Deliberately independent of any QE curve name — SSSC is solving for
QE, so it cannot be a key. The SessionResponseCache is a table
in gaia_xp_cache.sqlite storing the full JSON-serialised
SystemResponse for every run (gains, control points, per-stage
RMS, timestamp, B−V range). Each save appends; the Clear History
button issues a DELETE WHERE session_id = ?.
Not yet shipped. With Gaia DR4 (fainter XP coverage, so ∼10× more
calibrators per field) and multi-session data spanning a range of airmasses,
R(λ) decomposes as:
Hardware stays stable night-to-night (sensor, coatings, optics, filters);
atmosphere varies with airmass X and conditions. Imaging the
same rig across many nights at different airmasses lets the solver isolate
R_hardware(λ) and lock it in permanently. From then on
every calibration is Stage 2 speed with Stage 3+ accuracy,
using your measured hardware curve instead of a datasheet guess. The
curve also tracks aging naturally — mirror oxidation, filter wear,
and optical changes appear as drift in R_hardware(λ)
over time.
DR4 migration points in the code (filter-list bump to G ≤ 16.5,
gaiadr3 → gaiadr4 query change, lowered Stage-3 star
threshold, Stage-4 solver path, airmass header ingestion) are flagged
in the module header for the forthcoming port.