dispcraft
  • Home

User Guide

  • Getting Started
  • Using the Calibrated Models
  • Notebooks
    • Overview
    • 1. Subject & Instrument Model
    • 2. Data Introduction
      • 1. Setup and Imports
      • 2. Data Format
      • 3. Section 4 — Load and Explore
      • 4. Section 5 — Data Visualization
      • 5. Recap
    • 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
  • 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
  • 2. Data Introduction

Stage 2 — Data Introduction (PSF Ground-Test Dataset)¶

Exploration of the NISP FM ground-test PSF dataset (see 2-Intro_data/index.html).

Optical chain: Telescope Simulator → Collimator → Grism → Camera → Detector.

This notebook only loads, cleans, and visualizes the data. Calibrating the physical dispersion model is the next stage, not done here.

1. Setup and Imports¶

In [1]:
Copied!
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

DATA_DIR = Path("..") / "data"

# 7 optical configurations: grism (BGS/RGS) x GWA angle (nominal / -4 / +4 deg)
CONFIGS = [
    "bgs000_0",
    "rgs000_0",
    "rgs000_m4",
    "rgs000_p4",
    "rgs180_0",
    "rgs180_m4",
    "rgs180_p4",
]
from pathlib import Path import numpy as np import pandas as pd import matplotlib.pyplot as plt DATA_DIR = Path("..") / "data" # 7 optical configurations: grism (BGS/RGS) x GWA angle (nominal / -4 / +4 deg) CONFIGS = [ "bgs000_0", "rgs000_0", "rgs000_m4", "rgs000_p4", "rgs180_0", "rgs180_m4", "rgs180_p4", ]

2. Data Format¶

Each configuration has a *_first.csv (Fabry-Pérot emission lines, 1st diffraction order) and a *_zeroth.csv (two-blob model of the unresolved 0th order).

In [2]:
Copied!
df_first_sample = pd.read_csv(DATA_DIR / "rgs000_0_first.csv")
print("rgs000_0_first.csv", df_first_sample.shape)
print(df_first_sample.columns.tolist())
df_first_sample.head()
df_first_sample = pd.read_csv(DATA_DIR / "rgs000_0_first.csv") print("rgs000_0_first.csv", df_first_sample.shape) print(df_first_sample.columns.tolist()) df_first_sample.head()
rgs000_0_first.csv (16501, 11)
['psf_id', 'spectra_id', 'y_nisp', 'z_nisp', 'wavelength', 'cent_y', 'cent_z', 'sig_y', 'sig_z', 'theta', 'phi']
Out[2]:
psf_id spectra_id y_nisp z_nisp wavelength cent_y cent_z sig_y sig_z theta phi
0 420312678 1104849945 -150.664001 -126.503998 1211.33 -76.091584 -63.748050 0.000637 0.000637 -0.704651 -0.591664
1 588299741 1104849945 -150.664001 -126.503998 1211.33 -76.092143 -63.731574 0.000637 0.000637 -0.704651 -0.591664
2 1184322492 1104849945 -150.664001 -126.503998 1211.33 -76.085949 -63.761639 0.000637 0.000637 -0.704651 -0.591664
3 333255848 1401107208 -150.664001 -126.503998 1211.33 -76.039175 -63.816523 0.000664 0.000664 -0.704651 -0.591664
4 1517986096 1401107208 -150.664001 -126.503998 1211.33 -76.039677 -63.800123 0.000664 0.000664 -0.704651 -0.591664
In [3]:
Copied!
df_zeroth_sample = pd.read_csv(DATA_DIR / "rgs000_0_zeroth.csv")
print("rgs000_0_zeroth.csv", df_zeroth_sample.shape)
print(df_zeroth_sample.columns.tolist())
df_zeroth_sample.head()
df_zeroth_sample = pd.read_csv(DATA_DIR / "rgs000_0_zeroth.csv") print("rgs000_0_zeroth.csv", df_zeroth_sample.shape) print(df_zeroth_sample.columns.tolist()) df_zeroth_sample.head()
rgs000_0_zeroth.csv (1654, 12)
['psf_id', 'spectra_id', 'rank', 'wavelength', 'y_nisp', 'z_nisp', 'theta', 'phi', 'cent_y', 'cent_z', 'sig_y', 'sig_z']
Out[3]:
psf_id spectra_id rank wavelength y_nisp z_nisp theta phi cent_y cent_z sig_y sig_z
0 228205876 3597322 1 1250.0 62.203999 -126.503998 0.290938 -0.591664 30.509892 -79.010242 0.000725 0.000725
1 251124106 3597322 2 1850.0 62.203999 -126.503998 0.290938 -0.591664 30.512828 -78.806027 0.001714 0.001714
2 402503484 23821118 1 1250.0 39.848000 -132.084000 0.186377 -0.617760 19.379105 -81.870634 0.000752 0.000752
3 150621232 23821118 2 1850.0 39.848000 -132.084000 0.186377 -0.617760 19.383125 -81.661389 0.001487 0.001487
4 17572612 27542896 1 1250.0 120.325996 42.000000 0.562771 0.196442 59.285915 5.372802 0.000257 0.000257

3. Section 4 — Load and Explore¶

Model target for this dataset: (theta, phi, wavelength) -> (cent_y, cent_z).

load_spectra, median_per_spectrum, and zeroth_order_centers are promoted to dispcraft/measurement.py (tested in tests/test_measurement.py) — this notebook just imports and uses them.

In [4]:
Copied!
from dispcraft.measurement import load_spectra, median_per_spectrum, zeroth_order_centers
from dispcraft.measurement import load_spectra, median_per_spectrum, zeroth_order_centers
In [5]:
Copied!
# Sanity check: row counts before/after the sig_max quality filter, per config.
first_datasets = {}
for config in CONFIGS:
    raw = pd.read_csv(DATA_DIR / f"{config}_first.csv")
    filtered = load_spectra(DATA_DIR / f"{config}_first.csv")
    first_datasets[config] = filtered
    print(f"{config:12s} raw={len(raw):6d}  filtered={len(filtered):6d}  "
          f"spectra={filtered['spectra_id'].nunique()}")
# Sanity check: row counts before/after the sig_max quality filter, per config. first_datasets = {} for config in CONFIGS: raw = pd.read_csv(DATA_DIR / f"{config}_first.csv") filtered = load_spectra(DATA_DIR / f"{config}_first.csv") first_datasets[config] = filtered print(f"{config:12s} raw={len(raw):6d} filtered={len(filtered):6d} " f"spectra={filtered['spectra_id'].nunique()}")
bgs000_0     raw= 16467  filtered= 16023  spectra=155
rgs000_0     raw= 16501  filtered= 15982  spectra=171
rgs000_m4    raw= 14574  filtered= 14316  spectra=149
rgs000_p4    raw= 13920  filtered= 13731  spectra=144
rgs180_0     raw= 19127  filtered= 18587  spectra=201
rgs180_m4    raw= 13988  filtered= 13772  spectra=144
rgs180_p4    raw= 14082  filtered= 13812  spectra=144

4. Section 5 — Data Visualization¶

In [6]:
Copied!
def plot_field(df, title=""):
    """Scatter unique source positions in telescope-simulator angle space."""
    unique = df.drop_duplicates(subset="spectra_id")
    fig, ax = plt.subplots(figsize=(5, 5))
    ax.scatter(unique["theta"], unique["phi"], s=10)
    ax.set_xlabel("theta [deg]")
    ax.set_ylabel("phi [deg]")
    ax.set_aspect("equal")
    ax.set_title(title)
    plt.show()


plot_field(first_datasets["rgs000_0"], title="rgs000_0 -- field coverage")
def plot_field(df, title=""): """Scatter unique source positions in telescope-simulator angle space.""" unique = df.drop_duplicates(subset="spectra_id") fig, ax = plt.subplots(figsize=(5, 5)) ax.scatter(unique["theta"], unique["phi"], s=10) ax.set_xlabel("theta [deg]") ax.set_ylabel("phi [deg]") ax.set_aspect("equal") ax.set_title(title) plt.show() plot_field(first_datasets["rgs000_0"], title="rgs000_0 -- field coverage")
No description has been provided for this image
In [7]:
Copied!
def plot_centroids(df, color_by="wavelength", title=""):
    """Scatter PSF centroids in the detector mosaic frame R_MOS."""
    fig, ax = plt.subplots(figsize=(6, 5))
    sc = ax.scatter(df["cent_y"], df["cent_z"], c=df[color_by], cmap="rainbow", s=5)
    fig.colorbar(sc, ax=ax, label=color_by)
    ax.set_xlabel("cent_y [mm]")
    ax.set_ylabel("cent_z [mm]")
    ax.set_title(title)
    plt.show()


plot_centroids(first_datasets["rgs000_0"], title="rgs000_0 -- centroids in R_MOS")
def plot_centroids(df, color_by="wavelength", title=""): """Scatter PSF centroids in the detector mosaic frame R_MOS.""" fig, ax = plt.subplots(figsize=(6, 5)) sc = ax.scatter(df["cent_y"], df["cent_z"], c=df[color_by], cmap="rainbow", s=5) fig.colorbar(sc, ax=ax, label=color_by) ax.set_xlabel("cent_y [mm]") ax.set_ylabel("cent_z [mm]") ax.set_title(title) plt.show() plot_centroids(first_datasets["rgs000_0"], title="rgs000_0 -- centroids in R_MOS")
No description has been provided for this image
In [8]:
Copied!
def compare_gwa(configs, labels):
    """Overlay median centroids across GWA settings to show the GWA-induced shift.

    Sources are not paired 1:1 across configs (separate exposures), so the
    reported shift is a population-mean estimate, not a per-source delta.
    """
    fig, ax = plt.subplots(figsize=(6, 5))
    means = {}
    for config, label in zip(configs, labels):
        med = median_per_spectrum(load_spectra(DATA_DIR / f"{config}_first.csv"))
        ax.scatter(med["cent_y"], med["cent_z"], s=5, alpha=0.5, label=label)
        means[label] = (med["cent_y"].mean(), med["cent_z"].mean())
    ax.set_xlabel("cent_y [mm]")
    ax.set_ylabel("cent_z [mm]")
    ax.legend()
    ax.set_title("GWA angle comparison")
    plt.show()

    ref_label = labels[0]
    ref_y, ref_z = means[ref_label]
    for label in labels[1:]:
        y, z = means[label]
        print(f"{label} vs {ref_label}: mean shift dy={y - ref_y:+.3f} mm, "
              f"dz={z - ref_z:+.3f} mm")


compare_gwa(["rgs000_0", "rgs000_m4", "rgs000_p4"], ["nominal", "-4 deg", "+4 deg"])
def compare_gwa(configs, labels): """Overlay median centroids across GWA settings to show the GWA-induced shift. Sources are not paired 1:1 across configs (separate exposures), so the reported shift is a population-mean estimate, not a per-source delta. """ fig, ax = plt.subplots(figsize=(6, 5)) means = {} for config, label in zip(configs, labels): med = median_per_spectrum(load_spectra(DATA_DIR / f"{config}_first.csv")) ax.scatter(med["cent_y"], med["cent_z"], s=5, alpha=0.5, label=label) means[label] = (med["cent_y"].mean(), med["cent_z"].mean()) ax.set_xlabel("cent_y [mm]") ax.set_ylabel("cent_z [mm]") ax.legend() ax.set_title("GWA angle comparison") plt.show() ref_label = labels[0] ref_y, ref_z = means[ref_label] for label in labels[1:]: y, z = means[label] print(f"{label} vs {ref_label}: mean shift dy={y - ref_y:+.3f} mm, " f"dz={z - ref_z:+.3f} mm") compare_gwa(["rgs000_0", "rgs000_m4", "rgs000_p4"], ["nominal", "-4 deg", "+4 deg"])
No description has been provided for this image
-4 deg vs nominal: mean shift dy=+4.028 mm, dz=+8.410 mm
+4 deg vs nominal: mean shift dy=+1.744 mm, dz=+9.459 mm
In [9]:
Copied!
def compare_orders(config):
    """Overlay 1st- and 0th-order centroids for one config.

    1st- and 0th-order rows for the same config share spectra_id (same
    source exposure), so per-spectrum separation can be computed directly.
    """
    first = median_per_spectrum(load_spectra(DATA_DIR / f"{config}_first.csv"))
    zeroth = zeroth_order_centers(DATA_DIR / f"{config}_zeroth.csv")
    merged = first.merge(zeroth, on="spectra_id", suffixes=("_1st", "_0th"))

    fig, ax = plt.subplots(figsize=(6, 5))
    ax.scatter(first["cent_y"], first["cent_z"], s=5, alpha=0.4, label="1st order")
    ax.scatter(zeroth["cent_y"], zeroth["cent_z"], s=20, color="black", label="0th order")
    ax.set_xlabel("cent_y [mm]")
    ax.set_ylabel("cent_z [mm]")
    ax.legend()
    ax.set_title(f"Order comparison -- {config}")
    plt.show()

    sep = np.hypot(merged["cent_y_1st"] - merged["cent_y_0th"],
                    merged["cent_z_1st"] - merged["cent_z_0th"])
    print(f"{config}: mean 1st/0th-order separation = {sep.mean():.3f} mm "
          f"(n={len(merged)} matched lines)")


compare_orders("rgs000_0")
def compare_orders(config): """Overlay 1st- and 0th-order centroids for one config. 1st- and 0th-order rows for the same config share spectra_id (same source exposure), so per-spectrum separation can be computed directly. """ first = median_per_spectrum(load_spectra(DATA_DIR / f"{config}_first.csv")) zeroth = zeroth_order_centers(DATA_DIR / f"{config}_zeroth.csv") merged = first.merge(zeroth, on="spectra_id", suffixes=("_1st", "_0th")) fig, ax = plt.subplots(figsize=(6, 5)) ax.scatter(first["cent_y"], first["cent_z"], s=5, alpha=0.4, label="1st order") ax.scatter(zeroth["cent_y"], zeroth["cent_z"], s=20, color="black", label="0th order") ax.set_xlabel("cent_y [mm]") ax.set_ylabel("cent_z [mm]") ax.legend() ax.set_title(f"Order comparison -- {config}") plt.show() sep = np.hypot(merged["cent_y_1st"] - merged["cent_y_0th"], merged["cent_z_1st"] - merged["cent_z_0th"]) print(f"{config}: mean 1st/0th-order separation = {sep.mean():.3f} mm " f"(n={len(merged)} matched lines)") compare_orders("rgs000_0")
No description has been provided for this image
rgs000_0: mean 1st/0th-order separation = 12.755 mm (n=5328 matched lines)
In [10]:
Copied!
def plot_spectra(df, title=""):
    """Final view: dispersed centroids in R_MOS colored by wavelength."""
    plot_centroids(df, color_by="wavelength", title=title)


plot_spectra(first_datasets["rgs000_0"], title="rgs000_0 -- spectra in R_MOS")
def plot_spectra(df, title=""): """Final view: dispersed centroids in R_MOS colored by wavelength.""" plot_centroids(df, color_by="wavelength", title=title) plot_spectra(first_datasets["rgs000_0"], title="rgs000_0 -- spectra in R_MOS")
No description has been provided for this image

5. Recap¶

  • Optical chain: Telescope Simulator → Collimator → Grism → Camera → Detector.
  • Input frame: (theta, phi) (telescope-simulator angles, degrees). Output frame: (cent_y, cent_z) (detector mosaic frame R_MOS, mm).
  • Model target: (theta, phi, wavelength) -> (cent_y, cent_z).
  • Quality filter: sig_y < 0.05 mm and sig_z < 0.05 mm.
  • 7 configurations (BGS000/RGS000/RGS180 x GWA nominal/-4/+4), each with a 1st-order (Fabry-Pérot line) and 0th-order (two-blob) dataset.

Data loading/cleaning (load_spectra, median_per_spectrum, zeroth_order_centers) is promoted to dispcraft/measurement.py, tested in tests/test_measurement.py — same split as Stage 1: the physically meaningful, deterministic logic moves to the library, while exploratory visualization (plot_field, plot_centroids, compare_gwa, compare_orders, plot_spectra) stays inline in the notebook.

Calibrating the physical dispersion model against this data is the next stage -- not started here.

Previous Next

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