import numpy as np
import luxpy as lx
from luxpy.color.cri.iestm30.metrics import spd_to_ies_tm30_metrics
def tm30(spd):
return spd_to_ies_tm30_metrics(spd, cri_type='ies-tm30')
def scalar(x):
return float(np.squeeze(x))
def vec16(x):
return np.squeeze(x).tolist()
def print_section(title):
print()
print("=" * 70)
print(f" {title}")
print("=" * 70)
def compare(label, rust, python, tol):
ok = abs(rust - python) <= tol
flag = " OK" if ok else " FAIL <<<<<"
print(f" {label:<35} Rust={rust:8.3f} LuxPy={python:8.3f} Δ={rust-python:+.3f}{flag}")
def compare_bins(label, rust_vals, py_vals, tol):
print(f" {label}")
any_fail = False
for j, (r, p) in enumerate(zip(rust_vals, py_vals)):
ok = abs(r - p) <= tol
flag = "" if ok else " <<< FAIL"
if not ok:
any_fail = True
print(f" bin {j:2d} Rust={r:8.3f} LuxPy={p:8.3f} Δ={r-p:+.3f}{flag}")
if not any_fail:
print(f" All 16 bins within ±{tol}")
D65 = lx._CIE_ILLUMINANTS['D65']
F1 = lx._CIE_ILLUMINANTS['F1']
F2 = lx._CIE_ILLUMINANTS['F2']
F12 = lx._CIE_ILLUMINANTS['F12']
illuminants = [
("D65", D65),
("F1", F1),
("F2", F2),
("F12", F12),
]
rust_ref = {
"D65": {
"cct": 6504.0, "duv": 0.003, "Rf": 100.0, "Rg": 100.0,
"Rfhj": [100.0] * 16,
"Rcshj": [0.0] * 16,
"Rhshj": [0.0] * 16,
},
"F1": {
"cct": 6425.0, "duv": 0.0072, "Rf": 80.68, "Rg": 89.83,
"Rfhj": [64.52, 76.27, 70.52, 81.72, 86.43, 91.88, 88.93, 81.30,
86.87, 79.56, 82.99, 90.61, 86.35, 76.16, 69.85, 76.14],
"Rcshj": [-0.20, -0.13, -0.07, 0.02, 0.07, 0.01, -0.05, -0.10,
-0.11, -0.07, -0.01, 0.04, 0.07, 0.02, -0.09, -0.10],
"Rhshj": [-0.0248, 0.0843, 0.1564, 0.1126, 0.0579, -0.0427,
-0.0474, -0.0511, 0.0252, 0.0975, 0.0985, 0.0302,
-0.0833, -0.1430, -0.2770, -0.1015],
},
"F2": {
"cct": 4225.0, "duv": 0.0019, "Rf": 70.21, "Rg": 86.44,
"Rfhj": [60.20, 61.28, 52.52, 68.30, 79.50, 87.51, 76.74, 72.71,
76.13, 62.27, 69.57, 76.48, 81.32, 71.36, 63.60, 65.27],
"Rcshj": [-0.25, -0.18, -0.09, 0.05, 0.11, 0.04, -0.08, -0.15,
-0.17, -0.15, -0.04, 0.05, 0.11, 0.07, -0.06, -0.16],
"Rhshj": [-0.0221, 0.1400, 0.2444, 0.1963, 0.0915, -0.0673,
-0.1226, -0.0848, 0.0061, 0.1659, 0.1906, 0.1147,
-0.0806, -0.1467, -0.2635, -0.1703],
},
"F12": {
"cct": 3003.0, "duv": 0.0001, "Rf": 77.7, "Rg": 102.4,
"Rfhj": [78.222, 86.843, 80.612, 68.521, 72.478, 79.408, 72.551, 79.812,
82.356, 77.682, 74.540, 80.448, 82.391, 76.766, 79.243, 76.808],
"Rcshj": [-0.08510, -0.03160, -0.01216, 0.09247, 0.18373, 0.13567,
0.10277, -0.03677, -0.04746, -0.12181, -0.12371, 0.01845,
0.08819, 0.04632, 0.04186, -0.03594],
"Rhshj": [-0.04646, 0.03206, 0.09473, 0.17904, 0.12972, 0.00023,
-0.14516, -0.12447, -0.09830, 0.03149, 0.14218, 0.08155,
-0.02598, -0.06301, -0.09517, -0.10676],
},
}
for name, spd in illuminants:
print_section(f"CIE {name}")
d = tm30(spd)
py_cct = scalar(d['cct'])
py_duv = scalar(d['duv'])
py_Rf = scalar(d['Rf'])
py_Rg = scalar(d['Rg'])
py_Rfhj = vec16(d['Rfhj'])
py_Rcshj = vec16(d['Rcshj'])
py_Rhshj = vec16(d['Rhshj'])
r = rust_ref[name]
print()
print(" Scalar metrics (tolerance in parentheses):")
compare("CCT (K, ±10)", r["cct"], py_cct, 10.0)
compare("Duv (±0.001)", r["duv"], py_duv, 0.001)
compare("Rf (±0.6)", r["Rf"], py_Rf, 0.6)
compare("Rg (±0.6)", r["Rg"], py_Rg, 0.6)
print()
if name in ("F1", "F2"):
print(" Local metrics — Rust baseline is LuxPy itself (circular check):")
elif name == "F12":
print(" Local metrics — Rust baseline is IES TM-30 Spectral Calculator:")
else:
print(" Local metrics — D65 is its own reference (trivially perfect):")
compare_bins("Rf,hj (±10.0)", r["Rfhj"], py_Rfhj, 10.0)
compare_bins("Rcs,hj (±0.05)", r["Rcshj"], py_Rcshj, 0.05)
compare_bins("Rhs,hj (±0.04 rad)", r["Rhshj"], py_Rhshj, 0.04)
print()
print("=" * 70)
print(" Done. Rows marked <<< FAIL exceed tolerance.")
print(" F12 local metrics are cross-checked against the IES TM-30 Spectral")
print(" Calculator (independent reference). F1/F2 use LuxPy as their baseline")
print(" (circular, but confirms internal LuxPy consistency).")
print("=" * 70)