dispcraft.zeroth_dispersion¶
zeroth_dispersion ¶
0th-order two-blob chromatic separation: shared physics + NN residual.
Ground-test data shows a real, field-position-dependent separation between
the rank-1 (blue) and rank-2 (red) blobs
(dispcraft.measurement.zeroth_order_separation) that the physical model's
m=0 grating term alone doesn't predict (it's exactly zero at m=0).
But Stage 5 Phase 3 Step 4 found the prism's own chromatic dispersion
(Prism.deviation, unaffected by m) is NOT zero at m=0, and evaluating
it with each config's already-frozen first-order parameters
(dataclasses.replace(model, m_order=0), no new physics code needed)
explains a consistent ~27-29% of the observed separation across all 6 RGS
configs -- too consistent to be coincidence. Fitting a single, shared
material_k (Stage 3 found it "not identifiable from centroid-position
data alone" using only narrow-band first-order data; 0th order's two
widely-separated wavelengths, 1206nm/1892nm, supply exactly the missing
baseline) against all 6 configs' separations at once
(fit_shared_material_k) cuts the remaining RMSE ~11-14x (0.15-0.16mm ->
0.011-0.016mm), landing at material_k≈0.0143 (vs. nominal 0.004),
consistent to 3 significant figures across independent per-config fits.
So the "physical model" for 0th order is the same GroundTestModel/
parameters already fit against first-order data, evaluated at a different
m_order -- not a separate model. predict_zeroth_order_physical is that
shared-physics baseline; fit_zeroth_dispersion_model/
predict_zeroth_dispersion (Steps 1-3, unchanged) are the NN layer on top,
now expected to model the residual after this baseline (much smaller
than the raw separation they were originally fit to), not the full
separation -- mirrors Stage 5 Phase 2's physics-then-NN-residual pattern,
except here the "physics" step didn't exist until Step 4 identified it.
bgs000_0 was never part of Stage 3/4's calibration set -- excluded from
fit_shared_material_k, stays on the Step 3 standalone empirical model.
Stage 5 Phase 4 later gave it its own frozen first-order fit
(models/bgs000_0_fit.toml), but fusing it here still needs
_RANK_WAVELENGTHS_NM generalized from RGS's specific passband to a
per-grism one first (open item, Status_Report.md Section 10 item 9).
A real bug caught before trusting the result: the first version of
fit_shared_material_k reused dispcraft.calibration.cost_joint directly
on absolute-position data, matching the shared-physics prediction to the
0th order's actual absolute centroids. That's a different, much bigger,
unrelated problem (Stage 2 explicitly deferred calibrating the physical
model against 0th order's absolute position; this step doesn't reopen it)
-- the fit landed on a physically meaningless material_k≈-0.017 with
several-mm RMSE, an obvious red flag caught by comparing against the
~27-29%-explained finding above before trusting the number. Fixed by
fitting the separation instead (predict_zeroth_order_separation_physical),
which cancels out the absolute-registration mismatch and isolates the
actual chromatic effect -- recovers material_k≈0.0143 as validated during
planning.
Steps 1-3 (residual-structure diagnostic, NN architecture comparison,
fitting the per-config NN) are in
notebooks/5.3-Zeroth_Order_Dispersion.ipynb, Sections 1-11; Step 4 in
Sections 12+. Winning NN architecture (Step 2): per-dataset (not pooled per
grism instance -- dy's baseline sign depends on tilt direction, not just
grism identity, so pooling would average away real signal) joint 2-output
MLP on standardized (y0,z0), hidden_layer_sizes=(32,16), alpha=1e-3 --
the same architecture Phase 2 Step 2 picked independently for a different
target.
aggregate_by_position ¶
aggregate_by_position(df, value_cols=('dy', 'dz'), decimals=3)
Collapse spectra sharing (nearly) the same field position to one row.
Many spectra share the same (y_nisp, z_nisp) (repeated exposures at
one field angle) -- Phase 3 Step 2 found averaging them first reduces
noise (within-position std 2-2.3x smaller than between-position std)
before training, same rationale as dispcraft.field_calibration
.aggregate_by_position for the first-order residual, keyed on exact
rounded position here rather than spectra_id (0th order has no
repeated-wavelength rows to collapse first).
Source code in dispcraft/zeroth_dispersion.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
fit_shared_material_k ¶
fit_shared_material_k(dfs, fixed_by_cfg, model, k_init=0.004)
Fit one material_k, shared across every config in dfs, against
the 0th-order two-blob separation (not absolute position, see
predict_zeroth_order_separation_physical's docstring).
| Parameters: |
|
|---|
| Returns: |
|
|---|
Source code in dispcraft/zeroth_dispersion.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | |
fit_zeroth_dispersion_model ¶
fit_zeroth_dispersion_model(df, mlp_params=None)
Fit the field-dependent NN predicting the 0th-order two-blob
separation left over after the shared-physics baseline (module
docstring): Step 4 onwards, df["dy"]/df["dz"] should already have
predict_zeroth_order_physical's prediction subtracted (Steps 1-3
fit this directly against the raw separation instead, since Step 4's
physical baseline didn't exist yet).
| Parameters: |
|
|---|
| Returns: |
|
|---|
Source code in dispcraft/zeroth_dispersion.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
mad_outlier_mask ¶
mad_outlier_mask(x, n_mad=5.0)
Boolean mask (True = keep), median-absolute-deviation clip.
Stage 5 Phase 3 Step 1 found some 0th-order spectra have a single
unreplicated, mismatched-crossmatch measurement that survives
measurement.zeroth_order_separation's median-collapsing (it only
protects ranks with more than one exposure) -- producing dz/dy
of 20-30mm next to a clean population with MAD*1.4826 of ~0.01mm. A
wide n_mad clip isolates exactly these without touching the real
signal.
Source code in dispcraft/zeroth_dispersion.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
predict_zeroth_dispersion ¶
predict_zeroth_dispersion(y_nisp_mm, z_nisp_mm, nn_model)
(y0,z0) in mm -> (N,2) [dy,dz] predicted two-blob separation
residual, mm (add predict_zeroth_order_physical's output for the full
Step-4-onwards prediction; the full raw separation itself for Steps 1-3's
nn_models, which were fit with no physical baseline).
Source code in dispcraft/zeroth_dispersion.py
144 145 146 147 148 149 150 | |
predict_zeroth_order_physical ¶
predict_zeroth_order_physical(y_nisp_mm, z_nisp_mm, wavelength_nm, model, params)
Shared-physics 0th-order prediction: predict_centroids at
m_order=0 (the prism's own chromatic dispersion, unaffected by m),
using the same parameters already fit against first-order data.
| Parameters: |
|
|---|
| Returns: |
|
|---|
Source code in dispcraft/zeroth_dispersion.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | |
predict_zeroth_order_separation_physical ¶
predict_zeroth_order_separation_physical(y_nisp_mm, z_nisp_mm, model, params)
Physics-predicted rank2-rank1 (red-blue) separation at the RGS
passband's two edge wavelengths (_RANK_WAVELENGTHS_NM) -- the
quantity fit_shared_material_k actually fits against.
Deliberately not absolute position: predict_zeroth_order_physical's
absolute prediction (same offset_y_mm/offset_z_mm as first-order data)
does not match the 0th order's actual absolute centroids -- checked
while developing this step and found off by several mm, a much bigger,
unrelated registration problem Stage 2 explicitly deferred. The
separation between the two blobs of the same spectrum cancels that
out and isolates the actual chromatic effect this step targets.
| Returns: |
|
|---|
Source code in dispcraft/zeroth_dispersion.py
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |