Spectrophotometric Standard Star Calibration (SSSC)

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.

Published method. SSSC is a SetiAstro invention, documented in a formal whitepaper:
Marek, F. (2026). Spectrophotometric Standard Star Calibration: An Empirical System-Response Approach to Astrophotographic Color Calibration. Zenodo. doi:10.5281/zenodo.23107662. Wiki page: GitHub Wiki.

1. Using the tool

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

Step 0 — Plate solve

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.

Step 1 — Fetch stars & spectra

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(λ)).

The shared panel — filters and the Camera/Rig label

Three rows configure the optical chain:

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

Step 2 — Run SSSC calibration

The main action, with the standard options alongside:

Session memory — the long game

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.

Reading the diagnostics (four-panel figure)

After Step 2 the lower pane shows four panels; the suptitle names the stage reached, star count, and per-stage RMS waterfall:

Panel 1 — Solved R(λ) per channel

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.

Panel 2 — Residuals before / after

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.

Panel 3 — Calibration quality by stage (the waterfall)

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.

Panel 4 — Calibrator spectral coverage

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

After apply

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.

2. Technical details

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.

Third-party data used. Gaia DR3 XP calibrated spectra (ESA/DPAC) via 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.

2.1 The model

For every star i and channel c ∈ {R, G, B}:

\mathrm{measured}_c \;=\; k_c \,\int f_{\mathrm{star}}(\lambda)\,T^{c}_{\mathrm{filter}}(\lambda)\,R(\lambda)\,d\lambda

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.

2.2 Bootstrap-stage selection

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:

Stage = 3 if N ≥ 200 AND Δ(B-V) ≥ 1.5 2 if 50 ≤ N < 200 1 if N < 50

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

2.3 Stage 1 — scalar per-channel gains

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:

k_c=\mathrm{median}_i\!\left(\frac{\mathrm{meas}_{c/G}^{(i)}}{\mathrm{exp}_{c/G}^{(i)}}\right)

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.

2.4 Stage 2 — color-dependent band response

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

2.5 Stage 3 — full R(λ) solver

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:

R_c(\lambda)=\sum_{j=1}^{N_{\mathrm{ctrl}}} r_j^{(c)}\,\Lambda_j(\lambda)

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:

\hat{S}^{c}_i=\sum_{j=1}^{N_{\mathrm{ctrl}}} W_{ij}^{(c)}\,r_j^{(c)}\,,\quad W_{ij}^{(c)}=\int f_i(\lambda)\,T^{c}_{\mathrm{filter}}(\lambda)\,\Lambda_j(\lambda)\,d\lambda

Per-channel passband thresholds

Control points cover the wavelength range where T_filter^c is above a per-channel fraction of its peak:

ChannelThresholdRationale
R0.30 Tight enough to exclude cross-channel leakage, loose enough to cover the steep astro-filter rolloffs.
G0.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.
B0.30Same as R.

Loss and coordinate descent

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:

\mathcal{L}(\mathbf{r})=\sum_i\!\left(\frac{\mathrm{meas}_{R/G}^{(i)}}{k_R\hat{S}^{R}_i/\hat{S}^{G}_i}-1\right)^{\!2}+\sum_i\!\left(\frac{\mathrm{meas}_{B/G}^{(i)}}{k_B\hat{S}^{B}_i/\hat{S}^{G}_i}-1\right)^{\!2}

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:

r_j^{(c)}\leftarrow\mathrm{argmin}_{s\in\{1.1,\,1.2,\,0.9,\,0.8,\,1\}}\mathcal{L}\!\left(\,\ldots,\;s\cdot r_j^{(c)},\;\ldots\right)

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.

Prior-session seeding

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:

r_j^{(c),\mathrm{init}}=R^{(c)}_{\mathrm{prior}}\!\left(\lambda_j^{(c)}\right)\quad\text{if}\;N_{\mathrm{ctrl}}^{\mathrm{prior}}=N_{\mathrm{ctrl}}\;\text{and}\;\mathcal{L}_{\mathrm{seed}}\leq 5\,\mathcal{L}_{\mathrm{flat}}

Three checks gate the seed:

Strict-improvement guard

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:

\mathrm{if}\;\mathrm{RMS}_3>\mathrm{RMS}_2\;\Rightarrow\;\mathrm{revert\ to\ Stage\ 2}

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.

What drives the pixel correction

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.

2.6 Session identity & the response cache

Each solve is tagged with an 8-character session id derived from the filter+camera configuration:

\mathrm{session\_id}=\mathrm{SHA256}\!\left([R,G,B,\mathrm{LP}_1,\mathrm{LP}_2,\mathrm{cam}]\right)[:8]

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 = ?.

2.7 Stage 4 (future) — hardware/atmosphere separation

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:

R(λ) = R_hardware(λ) × R_atmosphere(λ, X)

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.