Flag the unverified spectrum wavelength axis; keep the fixture generator
The linear 380-1050 nm axis gives spectrum chromaticities 0.09-0.13 (xy) away from the instrument's own x, y, so TM-30 and spd.csv inherit an axis error until it is confirmed. tools/make_fixtures.py regenerates the golden data from the python-final driver. The release profile moves to the workspace that builds the binary.
This commit is contained in:
1 parent
54242da04b
commit
8f667dc6cf
3 files changed
+155
-2
No files matched your search
@@ -34,5 +34,3 @@ ctrlc = { version = "3.4", optional = true }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = { version = "1", features = ["preserve_order"] }
|
serde_json = { version = "1", features = ["preserve_order"] }
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
strip = true
|
|
||||||
+13
@@ -190,6 +190,19 @@ The 350 floats cover **380 nm to 1050 nm** at ~1.92 nm steps
|
|||||||
((1050-380) / (350-1) = 1.917 nm/step). Values are absolute spectral
|
((1050-380) / (350-1) = 1.917 nm/step). Values are absolute spectral
|
||||||
irradiance; the vendor software normalizes by dividing by the peak value.
|
irradiance; the vendor software normalizes by dividing by the peak value.
|
||||||
|
|
||||||
|
> **Unverified: the wavelength axis.** The linear 380-1050 nm mapping above
|
||||||
|
> is an assumption. Integrating the published lamp spectra with the CIE 1931
|
||||||
|
> observer on this axis gives chromaticities 0.09-0.13 (xy) away from the
|
||||||
|
> x, y the instrument itself reports in the same block (offsets 52/56); for
|
||||||
|
> L0011 the spectrum says x,y = 0.358, 0.295 against the instrument's 0.380,
|
||||||
|
> 0.382. No other linear range fits all lamps (380-780 nm comes closer but
|
||||||
|
> moves the blue LED peak to ~419 nm). The 350 points are probably on a
|
||||||
|
> non-linear (spectrometer pixel) axis. Everything computed from the
|
||||||
|
> spectrum (TM-30 Rf/Rg, the spd.csv files) inherits this until the axis is
|
||||||
|
> confirmed, e.g. against a spectrum exported by the vendor software with
|
||||||
|
> its wavelengths, or by fitting the axis so the spectrum reproduces the
|
||||||
|
> instrument's own XYZ.
|
||||||
|
|
||||||
### Unmapped Regions
|
### Unmapped Regions
|
||||||
|
|
||||||
Offsets 176-223, 256-271, and 1832-3903 contain additional data not yet
|
Offsets 176-223, 256-271, and 1832-3903 contain additional data not yet
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"""Golden fixtures for the Rust hpcs6500 driver, produced by the legacy Python driver.
|
||||||
|
|
||||||
|
Run it next to hpcs6500.py from tag python-final, with the USB captures in
|
||||||
|
C:\dev\HPCS\captures (not in git):
|
||||||
|
git show python-final:hpcs6500.py > /tmp/py/hpcs6500.py; cp tools/make_fixtures.py /tmp/py/
|
||||||
|
python /tmp/py/make_fixtures.py <out> # then copy <out>/fixtures and <out>/golden to tests/
|
||||||
|
pyfmt.json was trimmed to ~2000 cases afterwards.
|
||||||
|
|
||||||
|
Writes into <out>/:
|
||||||
|
fixtures/rNN_meas.bin, rNN_elec.bin raw blocks from real USB captures
|
||||||
|
golden/readings.json Python dict per fixture reading (floats as repr strings)
|
||||||
|
golden/format_*.txt format_reading / format_quick output
|
||||||
|
golden/csv_row.txt CSV row (without timestamp) as DictWriter writes it
|
||||||
|
golden/pyfmt.json repr / .6g / .2f / .0f / .1f of many floats
|
||||||
|
golden/parse_<capture>.txt `hpcs6500.py --parse` output, full and --quick --spectrum
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
import hpcs6500 as h # noqa: E402
|
||||||
|
|
||||||
|
out = Path(sys.argv[1])
|
||||||
|
captures = Path(r"C:\dev\HPCS\captures")
|
||||||
|
(out / "fixtures").mkdir(parents=True, exist_ok=True)
|
||||||
|
(out / "golden").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
pairs = [] # (name, meas, elec)
|
||||||
|
for f in sorted(captures.glob("*.pcap")):
|
||||||
|
if f.stat().st_size < 100:
|
||||||
|
continue
|
||||||
|
m = h.parse_pcap_messages(str(f))
|
||||||
|
b = [x for x in m if x["dir"] == "RX" and len(x["data"]) == 3904 and x["data"][:8] == b"HPCS6500"]
|
||||||
|
e = [x for x in m if x["dir"] == "RX" and len(x["data"]) == 1584]
|
||||||
|
for i, blk in enumerate(b):
|
||||||
|
el = e[i]["data"] if i < len(e) else None
|
||||||
|
harm = el is not None and abs(struct.unpack_from("<f", el, 544)[0] - 100) < 1
|
||||||
|
pairs.append((f.stem, i, blk["data"], el, harm))
|
||||||
|
|
||||||
|
print(len(pairs), "readings,", sum(p[4] for p in pairs), "with harmonics")
|
||||||
|
# pick: first, one with harmonics, one with zero electrical, and a few spread out
|
||||||
|
chosen = []
|
||||||
|
seen = set()
|
||||||
|
def pick(p):
|
||||||
|
key = (p[0], p[1])
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
chosen.append(p)
|
||||||
|
pick(pairs[0])
|
||||||
|
for p in pairs:
|
||||||
|
if p[4]:
|
||||||
|
pick(p)
|
||||||
|
break
|
||||||
|
for p in pairs:
|
||||||
|
if p[3] is not None and struct.unpack_from("<f", p[3], 8)[0] == 0:
|
||||||
|
pick(p)
|
||||||
|
break
|
||||||
|
for k in range(0, len(pairs), max(1, len(pairs) // 4)):
|
||||||
|
pick(pairs[k])
|
||||||
|
pick(pairs[-1])
|
||||||
|
|
||||||
|
dev = h.HPCS6500.__new__(h.HPCS6500)
|
||||||
|
|
||||||
|
|
||||||
|
def jsonable(v):
|
||||||
|
if isinstance(v, float):
|
||||||
|
return {"f": repr(v)}
|
||||||
|
if isinstance(v, list):
|
||||||
|
return [jsonable(x) for x in v]
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
readings = []
|
||||||
|
for n, (cap, i, meas, elec, harm) in enumerate(chosen, 1):
|
||||||
|
name = f"r{n:02d}"
|
||||||
|
(out / "fixtures" / f"{name}_meas.bin").write_bytes(meas)
|
||||||
|
if elec is not None:
|
||||||
|
(out / "fixtures" / f"{name}_elec.bin").write_bytes(elec)
|
||||||
|
r = dev._parse_measurement(meas)
|
||||||
|
if elec is not None:
|
||||||
|
r.update(dev._parse_electrical(elec))
|
||||||
|
readings.append({"name": name, "source": f"{cap}#{i}", "harmonics": harm,
|
||||||
|
"keys": list(r.keys()), "values": {k: jsonable(v) for k, v in r.items()}})
|
||||||
|
(out / "golden" / f"format_{name}.txt").write_text(h.format_reading(r), encoding="utf-8", newline="\n")
|
||||||
|
(out / "golden" / f"format_{name}_spectrum.txt").write_text(h.format_reading(r, show_spectrum=True), encoding="utf-8", newline="\n")
|
||||||
|
(out / "golden" / f"quick_{name}.txt").write_text(h.format_quick(r, n) + "\n" + h.format_quick(r), encoding="utf-8", newline="\n")
|
||||||
|
buf = io.StringIO()
|
||||||
|
fieldnames = ["timestamp"] + list(h.FIELDS.keys()) + list(h.ELECTRICAL_FIELDS.keys())
|
||||||
|
w = csv.DictWriter(buf, fieldnames=fieldnames, extrasaction="ignore")
|
||||||
|
w.writeheader()
|
||||||
|
row = {"timestamp": "T"}
|
||||||
|
row.update({k: v for k, v in r.items() if isinstance(v, (int, float))})
|
||||||
|
w.writerow(row)
|
||||||
|
(out / "golden" / f"csv_{name}.txt").write_bytes(buf.getvalue().encode())
|
||||||
|
(out / "golden" / "readings.json").write_text(json.dumps(readings, indent=1))
|
||||||
|
print("chosen:", [(c[0], c[1], c[4]) for c in chosen])
|
||||||
|
|
||||||
|
# --parse output for two real captures
|
||||||
|
for cap in ["usb_capture_20260204_123655_hub1", "usb_capture_20260204_134301_hub1"]:
|
||||||
|
for flags, suffix in (([], ""), (["--quick"], "_quick"), (["--harmonics"], "_harmonics")):
|
||||||
|
sys.argv = ["hpcs6500.py", *flags, "--parse", str(captures / f"{cap}.pcap")]
|
||||||
|
buf = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(buf):
|
||||||
|
h.main()
|
||||||
|
(out / "golden" / f"parse_{cap}{suffix}.txt").write_text(buf.getvalue(), encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
# float formatting golden
|
||||||
|
rng = random.Random(6500)
|
||||||
|
vals = [0.0, -0.0, 1.0, -1.0, 0.1, 0.5, 1.5, 2.5, 0.125, 0.375, 1e-5, 1e-4, 9.99995e-5, 123456.5, 999999.5,
|
||||||
|
1e15, 1e16, 1.2345678901234567e16, 1e22, 1e-7, 5e-324, 1.7976931348623157e308, 100.0, 380.0,
|
||||||
|
float("nan"), float("inf"), -float("inf"), 0.000123456789, 12345678.9, 0.999999, 0.9999995,
|
||||||
|
9.9999995, 99999.95, 1234567.0, 0.1 + 0.2]
|
||||||
|
vals += [380 + i * h.SPECTRUM_STEP for i in range(350)]
|
||||||
|
for meas_vals in readings:
|
||||||
|
for k, v in meas_vals["values"].items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
vals.append(float(v["f"]))
|
||||||
|
elif isinstance(v, list) and v and isinstance(v[0], dict):
|
||||||
|
vals.extend(float(x["f"]) for x in v[:60])
|
||||||
|
for _ in range(3000):
|
||||||
|
e = rng.uniform(-12, 18)
|
||||||
|
x = rng.choice([-1, 1]) * 10 ** e
|
||||||
|
vals.append(x)
|
||||||
|
vals.append(struct.unpack("<f", struct.pack("<f", x if abs(x) < 3e38 else 1.0))[0])
|
||||||
|
for _ in range(500):
|
||||||
|
vals.append(rng.randint(0, 10**7) / rng.choice([2, 4, 8, 10, 100, 1000, 16, 1024]))
|
||||||
|
rows = []
|
||||||
|
for v in vals:
|
||||||
|
rows.append({"bits": struct.unpack("<Q", struct.pack("<d", v))[0], "repr": repr(v), "g6": format(v, ".6g"),
|
||||||
|
"f2": format(v, ".2f") if abs(v) < 1e30 or math.isnan(v) or math.isinf(v) else None,
|
||||||
|
"f0": format(v, ".0f") if abs(v) < 1e30 or math.isnan(v) or math.isinf(v) else None,
|
||||||
|
"f1": format(v, ".1f") if abs(v) < 1e30 or math.isnan(v) or math.isinf(v) else None,
|
||||||
|
"round1": repr(round(v, 1)) if not (math.isnan(v) or math.isinf(v)) else None})
|
||||||
|
(out / "golden" / "pyfmt.json").write_text(json.dumps(rows))
|
||||||
|
print(len(rows), "float cases")
|
||||||
Reference in new issue
Block a user