dispcraft
  • Home

User Guide

  • Getting Started
  • Using the Calibrated Models
  • Notebooks
    • Overview
    • 1. Subject & Instrument Model
    • 2. Data Introduction
    • 3. ML Introduction
    • 4.1 ML Model Comparison
    • 4.2 Per-Dataset Pipeline
    • 4.3 Multi-Dataset Joint Fitting
    • 4.5 Comparison With Published Results
    • 5.1 PyTorch Migration
    • 5.2 Field-Dependent Parameters
    • 5.3 Zeroth-Order Dispersion
    • 5.4 BGS Model
    • 6. Status Report Assembly
    • 8. Chebyshev Residual Model
      • Setup
      • 1. Load frozen joint fits and data
      • 2. Held-out split and baselines
      • 3. Chebyshev residual, order 1
      • 4. Chebyshev residual, order 0
      • 5. Comparison table
      • 6. Findings
  • Status Report
  • Beginner Introduction (Slides)
  • Interactive Model (Webapp)

Reference

  • Euclid NISP Specs
  • Reference Paper Summary

API Reference

  • Overview
  • optics
  • measurement
  • calibration
  • field_calibration
  • zeroth_dispersion
  • chebyshev_residual
  • ml
  • model_registry
  • prediction
dispcraft
  • User Guide
  • Notebooks
  • 8. Chebyshev Residual Model

Stage 8 — Chebyshev-Polynomial Residual Model¶

Tests whether replacing Stage 4's generic MLPRegressor residual (hybrid_joint_ml) with a residual model that mirrors the reference paper's own functional form (arXiv:2506.08378, docs/2506.08378v2-nisp_grism_trace_model.md Eq. 2+3 — Chebyshev-in-wavelength whose coefficients are themselves Chebyshev-in-field-position) does better, worse, or comparably, on the same held-out protocol notebooks/6-Status_Report.ipynb established.

Starts from the joint 3-tier physical fit exactly as already frozen (models/joint_specific_fit_<cfg>.toml) — "the process that gave the best results" — and swaps only the residual-correction step (dispcraft/chebyshev_residual.py). Two orders are compared, in this order: order 1 (wave_order=spatial_order=1, 8 coefficients/axis) first, then order 0 (a single constant/axis — degenerate with the physical model's own offset_y_mm/offset_z_mm, included as a sanity floor). Scope: the 6 RGS configs only (matches the joint-fit process; bgs000_0 has its own separate single-dataset fit, not part of this comparison).

Setup¶

In [1]:
Copied!
import tomllib
from pathlib import Path

import numpy as np
import pandas as pd
import mlflow
from sklearn.model_selection import GroupShuffleSplit

import dispcraft.chebyshev_residual as cr
from dispcraft.calibration import ground_test_model_from_config, predict_centroids

REPO_ROOT = Path("..")
DATA_DIR = REPO_ROOT / "data"
MODELS_DIR = REPO_ROOT / "models"

RNG_SEED = 42  # same seed as 4.1/4.3/4.5/5.2/5.3/6 -- reproduces their splits exactly

CONFIGS = ["rgs000_0", "rgs000_m4", "rgs000_p4", "rgs180_0", "rgs180_m4", "rgs180_p4"]

with open(MODELS_DIR / "stage1_instrument.toml", "rb") as f:
    base_config = tomllib.load(f)
model = ground_test_model_from_config(base_config)

mlflow.set_tracking_uri(f"sqlite:///{(REPO_ROOT / 'mlflow.db').resolve()}")
import tomllib from pathlib import Path import numpy as np import pandas as pd import mlflow from sklearn.model_selection import GroupShuffleSplit import dispcraft.chebyshev_residual as cr from dispcraft.calibration import ground_test_model_from_config, predict_centroids REPO_ROOT = Path("..") DATA_DIR = REPO_ROOT / "data" MODELS_DIR = REPO_ROOT / "models" RNG_SEED = 42 # same seed as 4.1/4.3/4.5/5.2/5.3/6 -- reproduces their splits exactly CONFIGS = ["rgs000_0", "rgs000_m4", "rgs000_p4", "rgs180_0", "rgs180_m4", "rgs180_p4"] with open(MODELS_DIR / "stage1_instrument.toml", "rb") as f: base_config = tomllib.load(f) model = ground_test_model_from_config(base_config) mlflow.set_tracking_uri(f"sqlite:///{(REPO_ROOT / 'mlflow.db').resolve()}")
/home/zoubian/Workspace/dispers/dispcraft/.pixi/envs/default/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

1. Load frozen joint fits and data¶

Reuses notebooks/6-Status_Report.ipynb's own loading pattern: models/stage3_fit_<cfg>.toml for each config's excluded-spectra list (row count check), models/joint_specific_fit_<cfg>.toml for the frozen Tier 1+2+3 physical fit this notebook builds on unchanged.

In [2]:
Copied!
def load_toml(path):
    with open(path, "rb") as f:
        return tomllib.load(f)


def numeric_result(fit_result):
    return {k: v for k, v in fit_result.items() if not k.endswith("_bootstrap_std")}


from dispcraft.measurement import load_spectra, median_per_spectrum

dfs = {}
joint_fits = {}
for cfg in CONFIGS:
    indep = load_toml(MODELS_DIR / f"stage3_fit_{cfg}.toml")
    joint = load_toml(MODELS_DIR / f"joint_specific_fit_{cfg}.toml")

    df = median_per_spectrum(load_spectra(DATA_DIR / f"{cfg}_first.csv"))
    df = df[~df["spectra_id"].isin(indep["fit"]["excluded_spectra_ids"])].reset_index(drop=True)
    assert len(df) == indep["fit"]["n_points"], f"{cfg}: row count must match stage3_fit_{cfg}.toml"
    dfs[cfg] = df

    joint_fits[cfg] = {"fixed": {**joint["fit"]["fixed"], **numeric_result(joint["fit"]["result"])}}

print(f"loaded {len(dfs)} datasets, row counts: {[len(dfs[c]) for c in CONFIGS]}")
def load_toml(path): with open(path, "rb") as f: return tomllib.load(f) def numeric_result(fit_result): return {k: v for k, v in fit_result.items() if not k.endswith("_bootstrap_std")} from dispcraft.measurement import load_spectra, median_per_spectrum dfs = {} joint_fits = {} for cfg in CONFIGS: indep = load_toml(MODELS_DIR / f"stage3_fit_{cfg}.toml") joint = load_toml(MODELS_DIR / f"joint_specific_fit_{cfg}.toml") df = median_per_spectrum(load_spectra(DATA_DIR / f"{cfg}_first.csv")) df = df[~df["spectra_id"].isin(indep["fit"]["excluded_spectra_ids"])].reset_index(drop=True) assert len(df) == indep["fit"]["n_points"], f"{cfg}: row count must match stage3_fit_{cfg}.toml" dfs[cfg] = df joint_fits[cfg] = {"fixed": {**joint["fit"]["fixed"], **numeric_result(joint["fit"]["result"])}} print(f"loaded {len(dfs)} datasets, row counts: {[len(dfs[c]) for c in CONFIGS]}")
loaded 6 datasets, row counts: [5297, 4772, 4577, 6198, 4596, 4604]

2. Held-out split and baselines¶

Identical protocol to 6-Status_Report.ipynb's test_split (GroupShuffleSplit, test_size=0.2, random_state=42, grouped by spectra_id) — a field position never straddles train/test. Two baselines read for the comparison table: the physical joint fit alone (recomputed here, held-out), and hybrid_joint_ml's held-out RMSE (read back from the stage4 MLflow experiment, not retrained — matches 6's own stage4_hybrid_rmse).

In [3]:
Copied!
def test_split(df):
    gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=RNG_SEED)
    train_idx, test_idx = next(gss.split(df, groups=df["spectra_id"].values))
    return df.iloc[train_idx].reset_index(drop=True), df.iloc[test_idx].reset_index(drop=True)


def physical_residual(df, fixed):
    """r_y, r_z = pred_phys - data -- dispcraft.calibration.cost's own sign
    convention, and the one dispcraft.chebyshev_residual.fit_chebyshev_residual
    expects its input residual columns in (see that module's docstring)."""
    pred = predict_centroids(df["y_nisp"], df["z_nisp"], df["wavelength"], theta=[], free_names=[],
                              model=model, fixed=fixed)
    return pred[0] - df["cent_y"].values, pred[1] - df["cent_z"].values


def axis_rmse(r_y, r_z):
    return float(np.sqrt(np.mean(r_y**2))), float(np.sqrt(np.mean(r_z**2)))


train_test = {cfg: test_split(dfs[cfg]) for cfg in CONFIGS}

physical_rows = []
for cfg in CONFIGS:
    _, df_test = train_test[cfg]
    r_y, r_z = physical_residual(df_test, joint_fits[cfg]["fixed"])
    y, z = axis_rmse(r_y, r_z)
    physical_rows.append({"cfg": cfg, "physical_rmse_y_mm": y, "physical_rmse_z_mm": z})

physical_test = pd.DataFrame(physical_rows).set_index("cfg").loc[CONFIGS]
physical_test.round(5)
def test_split(df): gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=RNG_SEED) train_idx, test_idx = next(gss.split(df, groups=df["spectra_id"].values)) return df.iloc[train_idx].reset_index(drop=True), df.iloc[test_idx].reset_index(drop=True) def physical_residual(df, fixed): """r_y, r_z = pred_phys - data -- dispcraft.calibration.cost's own sign convention, and the one dispcraft.chebyshev_residual.fit_chebyshev_residual expects its input residual columns in (see that module's docstring).""" pred = predict_centroids(df["y_nisp"], df["z_nisp"], df["wavelength"], theta=[], free_names=[], model=model, fixed=fixed) return pred[0] - df["cent_y"].values, pred[1] - df["cent_z"].values def axis_rmse(r_y, r_z): return float(np.sqrt(np.mean(r_y**2))), float(np.sqrt(np.mean(r_z**2))) train_test = {cfg: test_split(dfs[cfg]) for cfg in CONFIGS} physical_rows = [] for cfg in CONFIGS: _, df_test = train_test[cfg] r_y, r_z = physical_residual(df_test, joint_fits[cfg]["fixed"]) y, z = axis_rmse(r_y, r_z) physical_rows.append({"cfg": cfg, "physical_rmse_y_mm": y, "physical_rmse_z_mm": z}) physical_test = pd.DataFrame(physical_rows).set_index("cfg").loc[CONFIGS] physical_test.round(5)
Out[3]:
physical_rmse_y_mm physical_rmse_z_mm
cfg
rgs000_0 0.15920 0.29802
rgs000_m4 0.22662 0.26412
rgs000_p4 0.18968 0.34455
rgs180_0 0.20131 0.29223
rgs180_m4 0.18399 0.31683
rgs180_p4 0.20515 0.28717
In [4]:
Copied!
def stage4_hybrid_rmse():
    """Per-axis, held-out RMSE of hybrid_joint_ml (joint physical fit +
    independent MLP(y)+MLP(z) residual, Stage 4 Phase 3 Section 4) -- read
    back from MLflow, not retrained. Mirrors notebooks/6-Status_Report
    .ipynb's own stage4_hybrid_rmse exactly."""
    exp = mlflow.get_experiment_by_name("stage4")
    assert exp is not None, "run notebooks/4.3-Multi_Dataset_Fitting.ipynb first to populate the 'stage4' experiment"
    runs = mlflow.search_runs(experiment_ids=[exp.experiment_id]).set_index("params.dataset")
    return runs.loc[CONFIGS, ["metrics.hybrid_rmse_y", "metrics.hybrid_rmse_z"]].rename(
        columns={"metrics.hybrid_rmse_y": "hybrid_ml_rmse_y_mm", "metrics.hybrid_rmse_z": "hybrid_ml_rmse_z_mm"}
    ).astype(float)


hybrid_ml_test = stage4_hybrid_rmse()
hybrid_ml_test.round(5)
def stage4_hybrid_rmse(): """Per-axis, held-out RMSE of hybrid_joint_ml (joint physical fit + independent MLP(y)+MLP(z) residual, Stage 4 Phase 3 Section 4) -- read back from MLflow, not retrained. Mirrors notebooks/6-Status_Report .ipynb's own stage4_hybrid_rmse exactly.""" exp = mlflow.get_experiment_by_name("stage4") assert exp is not None, "run notebooks/4.3-Multi_Dataset_Fitting.ipynb first to populate the 'stage4' experiment" runs = mlflow.search_runs(experiment_ids=[exp.experiment_id]).set_index("params.dataset") return runs.loc[CONFIGS, ["metrics.hybrid_rmse_y", "metrics.hybrid_rmse_z"]].rename( columns={"metrics.hybrid_rmse_y": "hybrid_ml_rmse_y_mm", "metrics.hybrid_rmse_z": "hybrid_ml_rmse_z_mm"} ).astype(float) hybrid_ml_test = stage4_hybrid_rmse() hybrid_ml_test.round(5)
Out[4]:
hybrid_ml_rmse_y_mm hybrid_ml_rmse_z_mm
params.dataset
rgs000_0 0.01761 0.06361
rgs000_m4 0.01408 0.06165
rgs000_p4 0.01744 0.08690
rgs180_0 0.01664 0.07965
rgs180_m4 0.02154 0.07430
rgs180_p4 0.02394 0.08309

3. Chebyshev residual, order 1¶

wave_order=spatial_order=1 (dispcraft.chebyshev_residual.n_chebyshev_terms(1,1) = 8 coefficients/axis). Fit on the train split's physical-fit residual (OLS, no random seed needed), evaluated on the held-out test split — same train/evaluate protocol as hybrid_joint_ml and the field-dependent tier's own held-out re-evaluation (6, Section 4).

In [5]:
Copied!
def chebyshev_held_out_rmse(cfg, wave_order, spatial_order):
    df_train, df_test = train_test[cfg]
    fixed = joint_fits[cfg]["fixed"]

    r_y_train, r_z_train = physical_residual(df_train, fixed)
    d_train = df_train.assign(r_y=r_y_train, r_z=r_z_train)

    cheb_model = cr.fit_chebyshev_residual(d_train, wave_order, spatial_order, *cr.RGS_PASSBAND_NM)

    pred_test = cr.predict_centroids_chebyshev(df_test["y_nisp"], df_test["z_nisp"], df_test["wavelength"],
                                                model, fixed, cheb_model)
    r_y_test = pred_test[0] - df_test["cent_y"].values
    r_z_test = pred_test[1] - df_test["cent_z"].values
    rmse_y, rmse_z = axis_rmse(r_y_test, r_z_test)
    return rmse_y, rmse_z, cheb_model


def run_chebyshev_order(wave_order, spatial_order, log_mlflow=True):
    if log_mlflow:
        mlflow.set_experiment("chebyshev_residual")
    rows, models_by_cfg = [], {}
    for cfg in CONFIGS:
        rmse_y, rmse_z, cheb_model = chebyshev_held_out_rmse(cfg, wave_order, spatial_order)
        rows.append({"cfg": cfg, "chebyshev_rmse_y_mm": rmse_y, "chebyshev_rmse_z_mm": rmse_z})
        models_by_cfg[cfg] = cheb_model
        if log_mlflow:
            with mlflow.start_run(run_name=f"{cfg}_order{wave_order}"):
                mlflow.log_params({"dataset": cfg, "wave_order": wave_order, "spatial_order": spatial_order,
                                    "n_terms_per_axis": cr.n_chebyshev_terms(wave_order, spatial_order)})
                mlflow.log_metrics({"rmse_y": rmse_y, "rmse_z": rmse_z, "combined": float(np.hypot(rmse_y, rmse_z))})
    table = pd.DataFrame(rows).set_index("cfg").loc[CONFIGS]
    return table, models_by_cfg


chebyshev1_test, chebyshev1_models = run_chebyshev_order(wave_order=1, spatial_order=1)
chebyshev1_test.round(5)
def chebyshev_held_out_rmse(cfg, wave_order, spatial_order): df_train, df_test = train_test[cfg] fixed = joint_fits[cfg]["fixed"] r_y_train, r_z_train = physical_residual(df_train, fixed) d_train = df_train.assign(r_y=r_y_train, r_z=r_z_train) cheb_model = cr.fit_chebyshev_residual(d_train, wave_order, spatial_order, *cr.RGS_PASSBAND_NM) pred_test = cr.predict_centroids_chebyshev(df_test["y_nisp"], df_test["z_nisp"], df_test["wavelength"], model, fixed, cheb_model) r_y_test = pred_test[0] - df_test["cent_y"].values r_z_test = pred_test[1] - df_test["cent_z"].values rmse_y, rmse_z = axis_rmse(r_y_test, r_z_test) return rmse_y, rmse_z, cheb_model def run_chebyshev_order(wave_order, spatial_order, log_mlflow=True): if log_mlflow: mlflow.set_experiment("chebyshev_residual") rows, models_by_cfg = [], {} for cfg in CONFIGS: rmse_y, rmse_z, cheb_model = chebyshev_held_out_rmse(cfg, wave_order, spatial_order) rows.append({"cfg": cfg, "chebyshev_rmse_y_mm": rmse_y, "chebyshev_rmse_z_mm": rmse_z}) models_by_cfg[cfg] = cheb_model if log_mlflow: with mlflow.start_run(run_name=f"{cfg}_order{wave_order}"): mlflow.log_params({"dataset": cfg, "wave_order": wave_order, "spatial_order": spatial_order, "n_terms_per_axis": cr.n_chebyshev_terms(wave_order, spatial_order)}) mlflow.log_metrics({"rmse_y": rmse_y, "rmse_z": rmse_z, "combined": float(np.hypot(rmse_y, rmse_z))}) table = pd.DataFrame(rows).set_index("cfg").loc[CONFIGS] return table, models_by_cfg chebyshev1_test, chebyshev1_models = run_chebyshev_order(wave_order=1, spatial_order=1) chebyshev1_test.round(5)
2026/09/01 11:02:55 INFO mlflow.tracking.fluent: Experiment with name 'chebyshev_residual' does not exist. Creating a new experiment.
Out[5]:
chebyshev_rmse_y_mm chebyshev_rmse_z_mm
cfg
rgs000_0 0.15129 0.12368
rgs000_m4 0.13342 0.12503
rgs000_p4 0.13767 0.13376
rgs180_0 0.14486 0.11838
rgs180_m4 0.14987 0.13861
rgs180_p4 0.10949 0.12855

4. Chebyshev residual, order 0¶

wave_order=spatial_order=0: a single constant/axis, degenerate with the physical model's own offset_y_mm/offset_z_mm — expected to land close to the physical-only baseline, included as a sanity floor rather than a serious candidate.

In [6]:
Copied!
chebyshev0_test, chebyshev0_models = run_chebyshev_order(wave_order=0, spatial_order=0)
chebyshev0_test.round(5)
chebyshev0_test, chebyshev0_models = run_chebyshev_order(wave_order=0, spatial_order=0) chebyshev0_test.round(5)
Out[6]:
chebyshev_rmse_y_mm chebyshev_rmse_z_mm
cfg
rgs000_0 0.16103 0.29910
rgs000_m4 0.22679 0.26415
rgs000_p4 0.19204 0.34580
rgs180_0 0.20221 0.29352
rgs180_m4 0.19200 0.31705
rgs180_p4 0.20926 0.29425

5. Comparison table¶

Same shape as Status_Report.md's existing tables: physical-only, the production hybrid_joint_ml, both Chebyshev orders, and the reference paper's held-out (Argon) figure.

In [7]:
Copied!
PX_MM_CORRECT = 0.018  # 18 um pixel pitch (Jahnke+2024, via Euclid-NISP-Specs.md), matches notebooks/6
paper_held_out_px = 0.5
paper_held_out_mm = paper_held_out_px * PX_MM_CORRECT

summary_rows = [
    {"model": "Physical model, joint fitting (Tier 1+2+3)",
     "rmse_y_mm": physical_test["physical_rmse_y_mm"].mean(), "rmse_z_mm": physical_test["physical_rmse_z_mm"].mean(),
     "note": "mean over 6 datasets, held-out test split"},
    {"model": "Hybrid, joint fitting + ML (hybrid_joint_ml, production default)",
     "rmse_y_mm": hybrid_ml_test["hybrid_ml_rmse_y_mm"].mean(), "rmse_z_mm": hybrid_ml_test["hybrid_ml_rmse_z_mm"].mean(),
     "note": "mean over 6 datasets, held-out test split; Status_Report.md Section 5"},
    {"model": "Hybrid, joint fitting + Chebyshev residual, order 1",
     "rmse_y_mm": chebyshev1_test["chebyshev_rmse_y_mm"].mean(), "rmse_z_mm": chebyshev1_test["chebyshev_rmse_z_mm"].mean(),
     "note": "8 coefficients/axis, mean over 6 datasets, held-out test split"},
    {"model": "Hybrid, joint fitting + Chebyshev residual, order 0",
     "rmse_y_mm": chebyshev0_test["chebyshev_rmse_y_mm"].mean(), "rmse_z_mm": chebyshev0_test["chebyshev_rmse_z_mm"].mean(),
     "note": "1 coefficient/axis (sanity floor), mean over 6 datasets, held-out test split"},
    {"model": "Reference (arXiv 2506.08378), held-out (Argon)",
     "rmse_y_mm": paper_held_out_mm, "rmse_z_mm": paper_held_out_mm,
     "note": f"combined figure, not axis-split in source ({paper_held_out_px} px = {paper_held_out_mm:.4f} mm)"},
]
comparison_table = pd.DataFrame(summary_rows).set_index("model")
comparison_table["combined_mm"] = np.hypot(comparison_table["rmse_y_mm"], comparison_table["rmse_z_mm"])
comparison_table.round(5)
PX_MM_CORRECT = 0.018 # 18 um pixel pitch (Jahnke+2024, via Euclid-NISP-Specs.md), matches notebooks/6 paper_held_out_px = 0.5 paper_held_out_mm = paper_held_out_px * PX_MM_CORRECT summary_rows = [ {"model": "Physical model, joint fitting (Tier 1+2+3)", "rmse_y_mm": physical_test["physical_rmse_y_mm"].mean(), "rmse_z_mm": physical_test["physical_rmse_z_mm"].mean(), "note": "mean over 6 datasets, held-out test split"}, {"model": "Hybrid, joint fitting + ML (hybrid_joint_ml, production default)", "rmse_y_mm": hybrid_ml_test["hybrid_ml_rmse_y_mm"].mean(), "rmse_z_mm": hybrid_ml_test["hybrid_ml_rmse_z_mm"].mean(), "note": "mean over 6 datasets, held-out test split; Status_Report.md Section 5"}, {"model": "Hybrid, joint fitting + Chebyshev residual, order 1", "rmse_y_mm": chebyshev1_test["chebyshev_rmse_y_mm"].mean(), "rmse_z_mm": chebyshev1_test["chebyshev_rmse_z_mm"].mean(), "note": "8 coefficients/axis, mean over 6 datasets, held-out test split"}, {"model": "Hybrid, joint fitting + Chebyshev residual, order 0", "rmse_y_mm": chebyshev0_test["chebyshev_rmse_y_mm"].mean(), "rmse_z_mm": chebyshev0_test["chebyshev_rmse_z_mm"].mean(), "note": "1 coefficient/axis (sanity floor), mean over 6 datasets, held-out test split"}, {"model": "Reference (arXiv 2506.08378), held-out (Argon)", "rmse_y_mm": paper_held_out_mm, "rmse_z_mm": paper_held_out_mm, "note": f"combined figure, not axis-split in source ({paper_held_out_px} px = {paper_held_out_mm:.4f} mm)"}, ] comparison_table = pd.DataFrame(summary_rows).set_index("model") comparison_table["combined_mm"] = np.hypot(comparison_table["rmse_y_mm"], comparison_table["rmse_z_mm"]) comparison_table.round(5)
Out[7]:
rmse_y_mm rmse_z_mm note combined_mm
model
Physical model, joint fitting (Tier 1+2+3) 0.19433 0.30049 mean over 6 datasets, held-out test split 0.35785
Hybrid, joint fitting + ML (hybrid_joint_ml, production default) 0.01854 0.07487 mean over 6 datasets, held-out test split; Sci... 0.07713
Hybrid, joint fitting + Chebyshev residual, order 1 0.13777 0.12800 8 coefficients/axis, mean over 6 datasets, hel... 0.18805
Hybrid, joint fitting + Chebyshev residual, order 0 0.19722 0.30231 1 coefficient/axis (sanity floor), mean over 6... 0.36095
Reference (arXiv 2506.08378), held-out (Argon) 0.00900 0.00900 combined figure, not axis-split in source (0.5... 0.01273

6. Findings¶

Order 0 reproduces the physical-only baseline almost exactly (0.361mm vs. 0.358mm combined) — confirms the expected degeneracy with the physical model's own offset_y_mm/offset_z_mm; not a competitive candidate, a sanity floor.

Order 1 is a genuine, substantial improvement over the raw physical fit (0.358mm -> 0.188mm combined, z especially: 0.300mm -> 0.128mm) — real low-order wavelength/field structure exists in the residual, consistent with Stage 5 Phase 2 Step 1's own linear-term diagnostic. Order 1 beats order 0 in all 6 configs, on both axes.

Neither order comes close to hybrid_joint_ml (0.077mm combined): order 1 is 2.4x worse, and its own gap to the reference paper's held-out figure (~21x) is far wider than hybrid_joint_ml's (~8.6x). An 8-coefficient linear basis per axis is evidently far less expressive than the generic MLP residual's effective capacity on this data. A genuine, informative negative result (same treatment Stage 5 Phase 2 gave the field-dependent tier), not a bug — order 2 and non-diagonal (wave_order, spatial_order) combinations are untested follow-ups, out of scope for this comparison (only orders 1 and 0 were agreed).

Recorded in docs/Status_Report.md Section 8 and frozen to models/chebyshev_residual_fit_<cfg>.toml via scripts/freeze_chebyshev_fits.py. hybrid_joint_ml remains the recommended production model (docs/User_Guide.md).

In [8]:
Copied!
print("=== Chebyshev residual vs. hybrid_joint_ml, held-out, mean over 6 datasets ===")
for label, tbl, cols in [
    ("physical (joint fit alone)", physical_test, ("physical_rmse_y_mm", "physical_rmse_z_mm")),
    ("hybrid_joint_ml", hybrid_ml_test, ("hybrid_ml_rmse_y_mm", "hybrid_ml_rmse_z_mm")),
    ("chebyshev order 1", chebyshev1_test, ("chebyshev_rmse_y_mm", "chebyshev_rmse_z_mm")),
    ("chebyshev order 0", chebyshev0_test, ("chebyshev_rmse_y_mm", "chebyshev_rmse_z_mm")),
]:
    y, z = tbl[cols[0]].mean(), tbl[cols[1]].mean()
    print(f"{label:32s} y={y:.4f} mm  z={z:.4f} mm  combined={np.hypot(y, z):.4f} mm")

print()
print(f"reference paper held-out (Argon, combined): {paper_held_out_mm:.4f} mm")
print("=== Chebyshev residual vs. hybrid_joint_ml, held-out, mean over 6 datasets ===") for label, tbl, cols in [ ("physical (joint fit alone)", physical_test, ("physical_rmse_y_mm", "physical_rmse_z_mm")), ("hybrid_joint_ml", hybrid_ml_test, ("hybrid_ml_rmse_y_mm", "hybrid_ml_rmse_z_mm")), ("chebyshev order 1", chebyshev1_test, ("chebyshev_rmse_y_mm", "chebyshev_rmse_z_mm")), ("chebyshev order 0", chebyshev0_test, ("chebyshev_rmse_y_mm", "chebyshev_rmse_z_mm")), ]: y, z = tbl[cols[0]].mean(), tbl[cols[1]].mean() print(f"{label:32s} y={y:.4f} mm z={z:.4f} mm combined={np.hypot(y, z):.4f} mm") print() print(f"reference paper held-out (Argon, combined): {paper_held_out_mm:.4f} mm")
=== Chebyshev residual vs. hybrid_joint_ml, held-out, mean over 6 datasets ===
physical (joint fit alone)       y=0.1943 mm  z=0.3005 mm  combined=0.3578 mm
hybrid_joint_ml                  y=0.0185 mm  z=0.0749 mm  combined=0.0771 mm
chebyshev order 1                y=0.1378 mm  z=0.1280 mm  combined=0.1881 mm
chebyshev order 0                y=0.1972 mm  z=0.3023 mm  combined=0.3610 mm

reference paper held-out (Argon, combined): 0.0090 mm
Previous Next

Built with MkDocs using a theme provided by Read the Docs.
dispers/dispcraft « Previous Next »