Instruments and Wavelength Calibration¶
This page describes how wavelength calibration for the Vis133M spectrometer is generated and used during runtime.
Building the calibration (one-time):
Reference CSV + reference FITS
↓
Polynomial wavelength model
↓
Stored in bh_wavecal.json
Using calibration on new data (per dataset):
- Load with Vis133M.from_files(fits_path) (no automatic CW).
- Inspect in pixel space: pick_frame(), plot_spectrum_pixels(), spectrum(..., channel=None).
- Estimate CW from a known line: estimate_cw_from_line(peak_pixel, line=...).
- Apply CW: apply_cw(cw_nm, ...); then use wavelength-calibrated plots and calibration_report().
The original CSV reference is only required once to build the calibration.
1. Reference calibration (instrument CSV)¶
The Vis133M spectrometer is initially calibrated using a dedicated instrument
wavecal CSV (for example 133mVis_wavcal.csv), which contains the per-channel
dispersion of the spectrometer.
The wavecal CSV contains:
- 1024 detector pixels in the dispersion direction
- multiple spatial channels, each containing wavelength values for all pixels
The instrument CSV is loaded using:
load_wavecal_csv()
which returns a wavelength matrix:
(n_channels, n_pixels)
For diagnostics or verification, the CSV can be converted to simple linear fits using:
csv_to_linear_formulas()
2. Polynomial wavelength model and calibration pixels¶
To avoid loading the CSV at runtime, the wavelength solution is compressed into an instrument dispersion polynomial.
For a chosen detector channel, in the calibration pixel coordinate system:
λ(x_cal) = P(x_cal - pixel_reference)
where
- P is the fitted polynomial
x_calis the pixel coordinate in the calibration imagepixel_referenceis the reference pixel index in that calibration image
The calibration cube used to build this model has a fixed number of pixels in the dispersion direction:
calibration_pixels = len(x_cal)
The helper function:
compute_calibration_from_reference(wavcal_csv, fits_path)
returns a dictionary including:
{
reference_cw_nm, # CW of the calibration image
coefficients, # polynomial coefficients for P
formula_type,
pixel_reference, # reference pixel in calibration coordinates
calibration_pixels # wavelength axis length of the calibration image
}
3. Stored calibration¶
The calibration is written to:
bh_molecule/_resources/bh_wavecal.json
Example schema:
reference_cw_nm– central wavelength of the calibration FITS imagecoefficients– polynomial coefficients for Ppixel_reference– reference pixel index in the calibration cubecalibration_pixels– number of calibration pixels along dispersion
The file is normally generated using the package-integrated builder module:
python -m bh_molecule.calibration_builder --csv 133mVis_wavcal.csv --fits path/to/reference.fits
This builder:
- reads the instrument wavecal CSV (e.g.
133mVis_wavcal.csv) - fits the polynomial
- writes
bh_wavecal.json
4. Loading data: Vis133M.from_files¶
The main entry point for new data is Vis133M.from_files(). It loads the FITS
cube and the polynomial calibration from package resources
(bh_molecule._resources/bh_wavecal.json). CW is never guessed; you must
set it explicitly.
API¶
Vis133M.from_files(
fits_path: str,
*,
cw: float | None = None,
line: str | float | None = None,
scale: float = 1.0,
)
cw |
line |
Behaviour |
|---|---|---|
| float | — | Use that CW; wavelength axis is applied immediately. |
| None | string | Look up wavelength in BALMER_LINES_NM; store for later (no CW applied). |
| None | float | Store as line wavelength (no CW applied). |
| both set | — | ValueError (provide only one). |
| neither | — | Load data with dispersion only; call apply_cw() later. |
Examples:
s = Vis133M.from_files("193791.fits") # dispersion only
s = Vis133M.from_files("193791.fits", cw=434.05) # use CW directly
s = Vis133M.from_files("193791.fits", line="H_gamma") # store line, no CW yet
s = Vis133M.from_files("193791.fits", line=434.0462) # store wavelength
5. Inspecting spectra before calibration¶
When CW is not set at load time, inspect the cube in pixel space to choose a frame and a peak pixel for CW estimation.
Pick a frame with signal¶
frame = s.pick_frame() # frame with maximum total signal (cube.sum(axis=(1,2)))
Spectrum in pixel space¶
# Single channel or sum over channels
pixels, intensity = s.spectrum(frame, channel=None, reduce="sum_channels")
# Or one channel:
pixels, intensity = s.spectrum(frame, channel=30)
With channel=None and reduce="sum_channels", the x-axis is pixel index
(not wavelength). Use this to identify the pixel of a known line (e.g. H-γ).
Plot spectrum (pixel x-axis)¶
fig = s.plot_spectrum_pixels(frame, show_peaks=True)
fig.show()
Uses Plotly; optional show_peaks=True marks local maxima with
scipy.signal.find_peaks.
6. Estimating CW from a known line¶
Once you have chosen the peak pixel of a reference line (e.g. H-γ), compute the central wavelength with estimate_cw_from_line. This does not change the instance; it only returns the CW value.
Balmer line constants¶
Wavelengths (nm, in air) for common reference lines:
from bh_molecule.constants import BALMER_LINES_NM
# {"H_alpha": 656.279, "H_beta": 486.133, "H_gamma": 434.0462, "H_delta": 410.174}
estimate_cw_from_line¶
cw_nm = s.estimate_cw_from_line(peak_pixel=876, line="H_gamma")
# or
cw_nm = s.estimate_cw_from_line(peak_pixel=876, wavelength=434.0462)
You must pass either line (string key in BALMER_LINES_NM) or
wavelength (float, nm). The calculation:
- Map data pixel → calibration pixel:
scale = calibration_pixels / data_pixels,
peak_cal = peak_pixel * scale,
x_rel = peak_cal - pixel_reference - Evaluate polynomial:
wl_ref = P(x_rel) - CW =
reference_cw_nm + (line_nm - wl_ref)
7. Applying CW¶
To apply the computed (or manual) CW and update the wavelength axis:
s.apply_cw(cw_nm)
Optional line metadata (for the calibration report):
s.apply_cw(cw_nm, line_name="H_gamma", peak_pixel=876, frame_used=frame)
If you pass line_name and omit line_wavelength, the wavelength is looked
up in BALMER_LINES_NM. apply_cw is only available for instances created
with from_files().
8. Runtime wavelength reconstruction (internal)¶
The dispersion formula is unchanged. With data pixel count N and
calibration_pixels Nc:
- Map data → calibration pixels:
x_cal = x_data * (Nc / N) - Wavelength:
λ(x_data) = P(x_cal - pixel_reference) + (CW - reference_cw_nm)
This is implemented by apply_polynomial_wavecal (and internally apply_wavecal). from_files and apply_cw use it to build the per-channel wavelength axis stored in the Vis133M instance.
9. Typical workflow (Jupyter)¶
from bh_molecule.instruments import Vis133M
s = Vis133M.from_files("193791.fits")
frame = s.pick_frame()
fig = s.plot_spectrum_pixels(frame, show_peaks=True)
fig.show()
cw = s.estimate_cw_from_line(peak_pixel=876, line="H_gamma")
s.apply_cw(cw, line_name="H_gamma", peak_pixel=876, frame_used=frame)
s.calibration_report()
fig = s.plot_spectrum_plotly(31, 30)
fig.show()
10. Calibration report and metadata¶
Instances created with from_files() store calibration metadata and can print a calibration report:
s.calibration_report()
Reported fields include:
- FITS file, data pixels, calibration pixels, binning factor
- Polynomial order
- If CW was applied: method (manual / reference line), line name and wavelength, peak pixel, frame used, CW used, reference CW, ΔCW
Attributes on the instance (when from_files):
cw_nm, cw_method, line_name, line_wavelength, peak_pixel,
frame_used, reference_cw_nm, delta_cw_nm, data_pixels,
calibration_pixels, binning_factor.
11. Legacy: CW from header or features¶
For scripts that do not use from_files(), the package still provides:
- get_cw_from_header(header) – scan FITS header for common CW keywords
(e.g. CWL, CENWAVE, WAVELEN, LAM_CEN, CRVAL1). - estimate_cw_from_features(spectrum, wavecal=..., diagnostic=False) –
estimate CW from the brightest pixel, with optional diagnostic plot.
Assumes that peak is a known line (e.g. H-γ); see wavecal module docstrings.
These are not used inside Vis133M.from_files(); the recommended path is explicit CW via estimate_cw_from_line + apply_cw as above.