Single entry point for both 1st- and 0th-order grism predictions.
Closes Status_Report.md §9/§10's "two separate function calls" gap:
1st-order position prediction (dispcraft.calibration.predict_centroids +
an optional per-config hybrid ML residual, docs/User_Guide.md §1-2) and
0th-order two-blob separation prediction (dispcraft.zeroth_dispersion's
shared-material_k physics baseline + a residual NN, docs/User_Guide.md
§3) were each wired together by hand, once per mode, directly in the guide
-- this module promotes that wiring into dispcraft/, so both orders share
one object and one calling convention instead of bespoke glue code per mode.
CalibratedGrism bundles the physics shared by both orders (one
GroundTestModel + one frozen per-config parameter set -- the same fit,
evaluated at m_order=1 for position and at m_order=0 for separation, per
dispcraft.zeroth_dispersion's module docstring) with each order's optional
ML residual. It does not change either model: predict_first_order is
exactly docs/User_Guide.md §1's predict_hybrid, and predict_zeroth_order
is exactly §3's predict_zeroth_order_full, moved here unchanged so a caller
writes them once instead of copying the recipe from documentation. Any
residual slot left unset means "physics only" for 1st order (degrades
gracefully); 0th order has no such fallback -- m_order=0's grating term is
exactly zero, so a residual NN is required to predict anything but a flat
zero (see dispcraft.zeroth_dispersion's module docstring).
load_calibrated_grism builds one from models/*.toml + the registered
residual models (dispcraft.model_registry, needs MLFLOW_TRACKING_TOKEN);
load_hybrid=False/load_zeroth=False skip the network calls entirely
(e.g. to build a physics-only CalibratedGrism for tests, or a config that
has no registered residual model, like a hand-rolled sensitivity study).
CalibratedGrism
dataclass
CalibratedGrism(model, params, hybrid_mlp_y=None, hybrid_mlp_z=None, hybrid_scaler_mean=None, hybrid_scaler_scale=None, zeroth_material_k=None, zeroth_residual_nn=None)
One config's calibrated model, both orders.
| Attributes: |
-
model, params
(GroundTestModel, dict -- the physics shared by both)
–
orders: nominal optics/material + this config's frozen fit
(Tier 1+2+3), e.g. models/joint_specific_fit_<config>.toml
-
hybrid_mlp_y, hybrid_mlp_z
((LitResidualRegressor, optional))
–
-- 1st-order per-axis residual MLPs; None for a physics-only model
-
hybrid_scaler_mean, hybrid_scaler_scale
(array-like, optional --)
–
standardization for (y, z, wavelength) the hybrid MLPs were
trained on; required together with hybrid_mlp_y/hybrid_mlp_z
-
zeroth_material_k
(float, optional -- physics-baseline `material_k`)
–
override for 0th order; None for configs with no frozen
first-order fit to evaluate at m=0 (currently only bgs000_0,
Stage 5 Phase 3), in which case predict_zeroth_order is NN-only
-
zeroth_residual_nn
(dispcraft.ml.LitResidualRegressor, optional --)
–
0th-order residual NN; required for predict_zeroth_order (see
module docstring -- unlike 1st order, there's no physics-only
fallback)
|
predict_first_order
predict_first_order(y_nisp_mm, z_nisp_mm, wavelength_nm)
(y, z, wavelength) [mm, mm, nm] -> (2, N) [cent_y, cent_z] mm.
Physical prediction alone if no hybrid MLPs were loaded; physical +
ML residual otherwise (docs/User_Guide.md §1's predict_hybrid).
Source code in dispcraft/prediction.py
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 | def predict_first_order(self, y_nisp_mm, z_nisp_mm, wavelength_nm):
"""(y, z, wavelength) [mm, mm, nm] -> (2, N) [cent_y, cent_z] mm.
Physical prediction alone if no hybrid MLPs were loaded; physical +
ML residual otherwise (`docs/User_Guide.md` §1's `predict_hybrid`).
"""
pred = predict_centroids(y_nisp_mm, z_nisp_mm, wavelength_nm,
theta=[], free_names=[], model=self.model, fixed=self.params)
if self.hybrid_mlp_y is None or self.hybrid_mlp_z is None:
return pred
X = (np.column_stack([y_nisp_mm, z_nisp_mm, wavelength_nm])
- self.hybrid_scaler_mean) / self.hybrid_scaler_scale
corr_y = ml_predict(self.hybrid_mlp_y, X).ravel()
corr_z = ml_predict(self.hybrid_mlp_z, X).ravel()
return np.array([pred[0] - corr_y, pred[1] - corr_z])
|
predict_zeroth_order
predict_zeroth_order(y_nisp_mm, z_nisp_mm)
(y, z) [mm] -> (N, 2) [dy, dz] mm, the rank2-rank1 two-blob
separation (docs/User_Guide.md §3's predict_zeroth_order_full).
Adds the shared-physics baseline only if zeroth_material_k is
set; NN-only otherwise (bgs000_0, see class docstring). Raises if
no residual NN was loaded at all -- there's no physics-only
fallback for this order (class docstring).
Source code in dispcraft/prediction.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129 | def predict_zeroth_order(self, y_nisp_mm, z_nisp_mm):
"""(y, z) [mm] -> (N, 2) [dy, dz] mm, the rank2-rank1 two-blob
separation (`docs/User_Guide.md` §3's `predict_zeroth_order_full`).
Adds the shared-physics baseline only if `zeroth_material_k` is
set; NN-only otherwise (`bgs000_0`, see class docstring). Raises if
no residual NN was loaded at all -- there's no physics-only
fallback for this order (class docstring).
"""
if self.zeroth_residual_nn is None:
raise ValueError("no zeroth-order residual NN loaded for this config -- the physical model alone "
"predicts exactly zero separation at m_order=0, see dispcraft.zeroth_dispersion")
resid = predict_zeroth_dispersion(y_nisp_mm, z_nisp_mm, self.zeroth_residual_nn)
if self.zeroth_material_k is None:
return resid
params_zeroth = {**self.params, "material_k": self.zeroth_material_k}
phys = predict_zeroth_order_separation_physical(y_nisp_mm, z_nisp_mm, self.model, params_zeroth)
return phys + resid
|
frozen_params
Merge a models/*_fit.toml's [fit.fixed] + [fit.result] into one
dict usable as predict_centroids's fixed=, dropping the
*_bootstrap_std uncertainty entries recorded alongside the fitted
values.
Source code in dispcraft/prediction.py
| def frozen_params(fit_toml):
"""Merge a `models/*_fit.toml`'s `[fit.fixed]` + `[fit.result]` into one
dict usable as `predict_centroids`'s `fixed=`, dropping the
`*_bootstrap_std` uncertainty entries recorded alongside the fitted
values."""
fixed = dict(fit_toml["fit"]["fixed"])
result = {k: v for k, v in fit_toml["fit"]["result"].items() if not k.endswith("_bootstrap_std")}
return {**fixed, **result}
|
load_calibrated_grism
load_calibrated_grism(config, models_dir='models', instrument_toml='stage1_instrument.toml', fit_toml=None, load_hybrid=True, load_zeroth=True, zeroth_material_k=DEFAULT_ZEROTH_MATERIAL_K, registry_version=None)
Build a CalibratedGrism for config from a frozen models/*.toml
fit plus the registered residual models (dispcraft.model_registry) --
one call replacing docs/User_Guide.md §1-3's hand-written per-mode
setup code.
| Parameters: |
-
config
(str -- e.g. "rgs000_0", "bgs000_0"; also the registered)
–
residual models' name prefix ("<config>-hybrid-mlp-y", etc.)
-
models_dir
(Path or str, default:
"models"
)
–
-
instrument_toml
(str -- nominal instrument config filename, relative to, default:
'stage1_instrument.toml'
)
–
models_dir; default is the RGS one -- pass
"stage1_bgs_instrument.toml" for bgs000_0
-
fit_toml
(str, optional -- frozen fit filename, relative to, default:
None
)
–
models_dir; default f"joint_specific_fit_{config}.toml" (the
RGS convention) -- pass "bgs000_0_fit.toml" for bgs000_0
-
load_hybrid
(bool -- fetch that order's registered, default:
True
)
–
residual model(s) from the registry (network call, needs
MLFLOW_TRACKING_TOKEN, see dispcraft.model_registry); set
False to build a physics-only CalibratedGrism for that order
without network access (e.g. in tests)
-
load_zeroth
(bool -- fetch that order's registered, default:
True
)
–
residual model(s) from the registry (network call, needs
MLFLOW_TRACKING_TOKEN, see dispcraft.model_registry); set
False to build a physics-only CalibratedGrism for that order
without network access (e.g. in tests)
-
zeroth_material_k
(float or None, default:
`DEFAULT_ZEROTH_MATERIAL_K`
)
–
-- physics-baseline coefficient for 0th order; pass None for a
config not fused with this shared-material_k baseline (currently
only bgs000_0 -- it has its own frozen first-order fit, but
fusing it needs dispcraft.zeroth_dispersion's hardcoded RGS rank
wavelengths generalized first, see that module's docstring)
-
registry_version
(int, optional -- pin one registered version for, default:
None
)
–
every model loaded here (default: latest of each)
|
Source code in dispcraft/prediction.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192 | def load_calibrated_grism(config, models_dir="models", instrument_toml="stage1_instrument.toml",
fit_toml=None, load_hybrid=True, load_zeroth=True,
zeroth_material_k=DEFAULT_ZEROTH_MATERIAL_K, registry_version=None):
"""Build a `CalibratedGrism` for `config` from a frozen `models/*.toml`
fit plus the registered residual models (`dispcraft.model_registry`) --
one call replacing `docs/User_Guide.md` §1-3's hand-written per-mode
setup code.
Parameters
----------
config : str -- e.g. "rgs000_0", "bgs000_0"; also the registered
residual models' name prefix (`"<config>-hybrid-mlp-y"`, etc.)
models_dir : Path or str, default "models"
instrument_toml : str -- nominal instrument config filename, relative to
`models_dir`; default is the RGS one -- pass
`"stage1_bgs_instrument.toml"` for `bgs000_0`
fit_toml : str, optional -- frozen fit filename, relative to
`models_dir`; default `f"joint_specific_fit_{config}.toml"` (the
RGS convention) -- pass `"bgs000_0_fit.toml"` for `bgs000_0`
load_hybrid, load_zeroth : bool -- fetch that order's registered
residual model(s) from the registry (network call, needs
`MLFLOW_TRACKING_TOKEN`, see `dispcraft.model_registry`); set
`False` to build a physics-only `CalibratedGrism` for that order
without network access (e.g. in tests)
zeroth_material_k : float or None, default `DEFAULT_ZEROTH_MATERIAL_K`
-- physics-baseline coefficient for 0th order; pass `None` for a
config not fused with this shared-`material_k` baseline (currently
only `bgs000_0` -- it has its own frozen first-order fit, but
fusing it needs `dispcraft.zeroth_dispersion`'s hardcoded RGS rank
wavelengths generalized first, see that module's docstring)
registry_version : int, optional -- pin one registered version for
every model loaded here (default: latest of each)
Returns
-------
CalibratedGrism
"""
models_dir = Path(models_dir)
fit_toml = fit_toml or f"joint_specific_fit_{config}.toml"
with open(models_dir / instrument_toml, "rb") as f:
model = ground_test_model_from_config(tomllib.load(f))
with open(models_dir / fit_toml, "rb") as f:
params = frozen_params(tomllib.load(f))
hybrid_mlp_y = hybrid_mlp_z = hybrid_scaler_mean = hybrid_scaler_scale = None
if load_hybrid:
hybrid_mlp_y, extra = load_model(f"{config}-hybrid-mlp-y", version=registry_version)
hybrid_mlp_z, _ = load_model(f"{config}-hybrid-mlp-z", version=registry_version)
hybrid_scaler_mean, hybrid_scaler_scale = extra["scaler_mean"], extra["scaler_scale"]
zeroth_residual_nn = None
if load_zeroth:
zeroth_residual_nn, _ = load_model(f"{config}-zeroth-residual-nn", version=registry_version)
return CalibratedGrism(
model=model, params=params,
hybrid_mlp_y=hybrid_mlp_y, hybrid_mlp_z=hybrid_mlp_z,
hybrid_scaler_mean=hybrid_scaler_mean, hybrid_scaler_scale=hybrid_scaler_scale,
zeroth_material_k=zeroth_material_k, zeroth_residual_nn=zeroth_residual_nn,
)
|