Files
lamp-testbench/crates/tm30/src/data.rs
T
grabowski 2a3f28001c Lamp testbench in Rust around the HPCS 6500
lampbench station / export / label / tm30 / calibrate replace the Python
lamp_station.py, lamp_export.py, label_printer.py, tm30.py and
calibration.py that lived in HPCS6500-py. The driver is a submodule
(HPCS6500/, like mppt-testbench). Workspace crates: tm30 (TM-30-18 port of
colour-science 0.4.7), brother-ql (raster protocol, byte-identical to
brother_ql) and lamp-label (QR + ID labels for the QL-820NWB).

Bundles are byte-identical to lamp_export.py's (golden tests); the station
runs end to end in a test against the simulated instrument.
2026-10-22 17:26:14 +07:00

140 lines
5.3 KiB
Rust

//! Embedded spectral tables (extracted by `tools/extract_data.py`).
//!
//! All tables are the *effective* arrays colour-science 0.4.7 works with on
//! the TM-30 path (after its Sprague re-alignment), stored as CSV with
//! shortest round-trip float formatting, parsed once on first use.
use std::sync::OnceLock;
/// First wavelength of the TM-30 working grid (nm).
pub(crate) const WL_START: usize = 380;
/// Last wavelength of the TM-30 working grid (nm).
pub(crate) const WL_END: usize = 780;
/// Number of samples of the 380..=780 nm / 1 nm working grid.
pub(crate) const N_WL: usize = WL_END - WL_START + 1;
/// First wavelength of colour's default spectral shape (Planckian table).
pub(crate) const WL_DEFAULT_START: usize = 360;
/// Number of samples of colour's default spectral shape 360..=780 nm / 1 nm.
pub(crate) const N_WL_DEFAULT: usize = WL_END - WL_DEFAULT_START + 1;
/// Number of CIE 2017 test colour samples.
pub(crate) const N_TCS: usize = 99;
const CMF_1931_360_780: &str = include_str!("../data/cmf_cie1931_2deg_360_780.csv");
const CMF_1931_380_780: &str = include_str!("../data/cmf_cie1931_2deg_380_780.csv");
const CMF_1964_380_780: &str = include_str!("../data/cmf_cie1964_10deg_380_780.csv");
const D_SERIES_380_780: &str = include_str!("../data/d_series_basis_380_780.csv");
const TCS_380_780: &str = include_str!("../data/tcs_cie2017_380_780.csv");
const CIECAM02_MATRICES: &str = include_str!("../data/ciecam02_matrices.csv");
pub(crate) type Mat3 = [[f64; 3]; 3];
pub(crate) struct Tables {
/// CIE 1931 2° CMFs on 360..=780 nm (used for the Ohno Planckian table).
pub cmf2_default: Vec<[f64; 3]>,
/// CIE 1931 2° CMFs on 380..=780 nm (test CCT, reference normalisation).
pub cmf2: Vec<[f64; 3]>,
/// CIE 1964 10° CMFs on 380..=780 nm (all CIECAM02 colorimetry).
pub cmf10: Vec<[f64; 3]>,
/// CIE D-series basis functions S0, S1, S2 on 380..=780 nm.
pub d_basis: Vec<[f64; 3]>,
/// CIE 2017 TCS reflectances, `tcs[sample][wavelength]`.
pub tcs: Vec<Vec<f64>>,
/// CAT02 matrix.
pub cat02: Mat3,
/// `MATRIX_XYZ_TO_HPE @ inv(CAT02)` exactly as computed by numpy.
pub hpe_cat02_inv: Mat3,
}
fn parse_rows(csv: &str, name: &str) -> Vec<Vec<f64>> {
csv.lines()
.skip(1)
.filter(|l| !l.trim().is_empty())
.map(|line| {
line.split(',')
.map(|field| {
field
.trim()
.parse::<f64>()
.unwrap_or_else(|e| panic!("{name}: bad number {field:?}: {e}"))
})
.collect()
})
.collect()
}
/// Parse a `wavelength,c1,c2,c3` table and check the wavelength column.
fn parse_tristimulus(csv: &str, name: &str, start: usize, n: usize) -> Vec<[f64; 3]> {
let rows = parse_rows(csv, name);
assert_eq!(rows.len(), n, "{name}: unexpected row count");
rows.iter()
.enumerate()
.map(|(i, r)| {
assert_eq!(r.len(), 4, "{name}: unexpected column count");
assert_eq!(r[0], (start + i) as f64, "{name}: unexpected wavelength");
[r[1], r[2], r[3]]
})
.collect()
}
fn parse_tcs() -> Vec<Vec<f64>> {
let rows = parse_rows(TCS_380_780, "tcs");
assert_eq!(rows.len(), N_WL, "tcs: unexpected row count");
let mut tcs = vec![vec![0.0; N_WL]; N_TCS];
for (w, r) in rows.iter().enumerate() {
assert_eq!(r.len(), N_TCS + 1, "tcs: unexpected column count");
assert_eq!(r[0], (WL_START + w) as f64, "tcs: unexpected wavelength");
for (s, sample) in tcs.iter_mut().enumerate() {
sample[w] = r[s + 1];
}
}
tcs
}
fn parse_matrices() -> (Mat3, Mat3) {
let mut cat02 = None;
let mut hpe = None;
for line in CIECAM02_MATRICES.lines().skip(1) {
let mut fields = line.split(',');
let Some(name) = fields.next() else { continue };
let v: Vec<f64> = fields
.map(|f| {
f.trim()
.parse::<f64>()
.expect("ciecam02 matrices: bad number")
})
.collect();
assert_eq!(v.len(), 9, "ciecam02 matrices: expected 9 values");
let m = [[v[0], v[1], v[2]], [v[3], v[4], v[5]], [v[6], v[7], v[8]]];
match name.trim() {
"CAT02" => cat02 = Some(m),
"HPE_CAT02_INV" => hpe = Some(m),
other => panic!("ciecam02 matrices: unknown matrix {other}"),
}
}
(
cat02.expect("CAT02 missing"),
hpe.expect("HPE_CAT02_INV missing"),
)
}
pub(crate) fn tables() -> &'static Tables {
static TABLES: OnceLock<Tables> = OnceLock::new();
TABLES.get_or_init(|| {
let (cat02, hpe_cat02_inv) = parse_matrices();
Tables {
cmf2_default: parse_tristimulus(
CMF_1931_360_780,
"cmf 1931 360-780",
WL_DEFAULT_START,
N_WL_DEFAULT,
),
cmf2: parse_tristimulus(CMF_1931_380_780, "cmf 1931 380-780", WL_START, N_WL),
cmf10: parse_tristimulus(CMF_1964_380_780, "cmf 1964 380-780", WL_START, N_WL),
d_basis: parse_tristimulus(D_SERIES_380_780, "d-series basis", WL_START, N_WL),
tcs: parse_tcs(),
cat02,
hpe_cat02_inv,
}
})
}