Initial commit

This commit is contained in:
2026-09-10 15:11:55 +01:00
commit d62864bd89
10 changed files with 857 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
"""Shared spectrum I/O and relative spectral-response correction.
A "spectrum CSV" throughout this project is two columns with a one-line header:
`wavelength_nm,value`. Measured spectra, dark frames and responsivity.csv all
share that shape, so one loader serves them all.
"""
import numpy as np
_H, _C, _K = 6.62607015e-34, 2.99792458e8, 1.380649e-23
def planck(wl_nm, temperature):
"""Blackbody spectral radiance vs wavelength, arbitrary scale.
A greybody with flat emissivity. Used both as a calibration reference (a
tungsten lamp near this shape) and as the fit model for a hot emitter. A
real filament's emissivity falls slowly toward the NIR, a gentle tilt we
accept for relative work.
"""
lam = wl_nm * 1e-9
return 1.0 / (lam ** 5 * np.expm1(_H * _C / (lam * _K * temperature)))
def load_csv(path):
"""Load a two-column spectrum CSV -> (wavelength_nm, value) arrays."""
wl, value = np.loadtxt(path, delimiter=",", skiprows=1, unpack=True)
return wl, value
def add_calibration_args(parser):
"""Add the shared --response / --dark options to an argparse parser."""
parser.add_argument("-r", "--response", metavar="CSV", default=None,
help="responsivity.csv from calibrate_response.py; "
"output becomes relative irradiance")
parser.add_argument("--dark", metavar="CSV", default=None,
help="dark_model.csv from capture_dark.py; the dark is "
"reconstructed for this measurement's integration time")
def dark_at(dark_path, integration_us):
"""Reconstruct the dark at an integration time from a dark_model.csv.
Returns a (wavelength_nm, counts) tuple, or None if `dark_path` is falsy.
The model is per-pixel `offset + rate * t_us` (see capture_dark.py).
"""
if not dark_path:
return None
wl, offset, rate = np.loadtxt(dark_path, delimiter=",", skiprows=1, unpack=True)
return wl, offset + rate * integration_us
def apply_response(wl, counts, cal, dark=None):
"""Response-correct `counts` on grid `wl` into relative-irradiance units.
corrected = (counts - dark) / R, with dark and R interpolated onto `wl`.
`cal` and `dark` are (wl, value) tuples; `dark` may be None. The dark here
MUST match the one used to build the calibration. Non-positive or non-finite
R (i.e. outside the calibrated band) yields NaN.
"""
signal = counts.astype(float)
if dark is not None:
signal = signal - np.interp(wl, dark[0], dark[1])
r = np.interp(wl, cal[0], cal[1])
r = np.where(np.isfinite(r) & (r > 0), r, np.nan)
return signal / r