1"""Load-cell tare reconstruction in the viewer.
3Run: cd webviewer && .venv/bin/python -m pytest backend/test_lc_tare.py -q
5Hermetic — no elodin-db and no real run. Elodin archives ABSOLUTE force forever; the tare is
6display state the backend recorded beside the run, and these tests pin the replay of it.
8Every case names the wrong number it prevents:
9 * a step applied across the whole array makes the PRE-tare part of every run read wrong;
10 * a missed 'recal' line makes everything after a re-fit read wrong;
11 * synthesising a tared twin unconditionally makes an untared run look tared;
12 * synthesising in build_index instead of at serve time means a run exported before its sidecar
13 landed never shows its tare, with the cached index quietly winning.
16from __future__
import annotations
24from .
import config, export_cache, lc_tare, run_config, series
28def run(tmp_path, monkeypatch):
29 """A scratch run: parquet cache and ELODIN_DIR both under tmp_path."""
30 cache = tmp_path /
"cache"
32 elodin = tmp_path /
"elodin"
33 (elodin /
"daq_20260914_120000").mkdir(parents=
True)
34 monkeypatch.setattr(config,
"ELODIN_DIR", elodin)
35 monkeypatch.setattr(export_cache,
"cache_dir",
lambda run_id: cache)
38 series.load_series.cache_clear()
39 series.sensor_clock.cache_clear()
40 return {
"id":
"daq_20260914_120000",
"cache": cache,
"elodin": elodin}
43def write_gross(run, entity="LC2_Cal.CH1", times=(100.0, 200.0, 300.0), values=(50.0, 51.0, 52.0)):
45 "time": pd.to_datetime(np.array(times) * 1e9, unit=
"ns"),
46 "value": list(values),
48 df.to_parquet(run[
"cache"] / f
"{entity}.force_kg.parquet")
52 p = run[
"elodin"] / run[
"id"] /
"lc_tare.jsonl"
53 p.write_text(
"".join(json.dumps(l) +
"\n" for l
in lines))
63 {
"entity":
"LC2_Cal.CH1",
"uid": 4201,
"event":
"set",
64 "offsetKg": 20.0,
"adcAtTare": 1000,
"appliedAtMs": 250_000},
66 t, v = series.load_series(run[
"id"],
"LC2_Cal.CH1.force_kg_tared")
67 assert list(t) == [100.0, 200.0, 300.0]
68 assert v == pytest.approx([50.0, 51.0, 32.0])
76 {
"entity":
"LC2_Cal.CH1",
"event":
"set",
"offsetKg": 20.0,
"appliedAtMs": 150_000},
77 {
"entity":
"LC2_Cal.CH1",
"event":
"recal",
"offsetKg": 22.0,
"appliedAtMs": 250_000},
79 _, v = series.load_series(run[
"id"],
"LC2_Cal.CH1.force_kg_tared")
80 assert v == pytest.approx([50.0, 31.0, 30.0])
86 {
"entity":
"LC2_Cal.CH1",
"event":
"set",
"offsetKg": 20.0,
"appliedAtMs": 150_000},
87 {
"entity":
"LC2_Cal.CH1",
"event":
"clear",
"offsetKg": 0.0,
"appliedAtMs": 250_000},
89 _, v = series.load_series(run[
"id"],
"LC2_Cal.CH1.force_kg_tared")
90 assert v == pytest.approx([50.0, 31.0, 52.0])
97 {
"entity":
"LC2_Cal.CH1",
"event":
"set",
"offsetKg": 20.0,
"appliedAtMs": 150_000},
99 _, gross = series.load_series(run[
"id"],
"LC2_Cal.CH1.force_kg")
100 assert gross == pytest.approx([50.0, 51.0, 52.0])
108 {
"entity":
"LC2_Cal.CH1",
"event":
"set",
"offsetKg": 20.0,
"appliedAtMs": 150_000},
110 _, gross = series.load_series(run[
"id"],
"LC2_Cal.CH1.force_kg")
111 _, tared = series.load_series(run[
"id"],
"LC2_Cal.CH1.force_kg_tared")
112 assert list(gross) != list(tared)
121 with pytest.raises(FileNotFoundError):
122 series.load_series(run[
"id"],
"LC2_Cal.CH1.force_kg_tared")
123 idx = run_config.annotate({
"components": [
124 {
"name":
"LC2_Cal.CH1.force_kg",
"entity":
"LC2_Cal.CH1",
"field":
"force_kg"},
126 assert [c[
"name"]
for c
in idx[
"components"]] == [
"LC2_Cal.CH1.force_kg"]
132 {
"entity":
"LC2_Cal.CH1",
"event":
"set",
"offsetKg": 20.0,
"appliedAtMs": 150_000},
134 idx = run_config.annotate({
"components": [
135 {
"name":
"LC2_Cal.CH1.force_kg",
"entity":
"LC2_Cal.CH1",
"field":
"force_kg"},
136 {
"name":
"LC2_Cal.CH2.force_kg",
"entity":
"LC2_Cal.CH2",
"field":
"force_kg"},
138 names = [c[
"name"]
for c
in idx[
"components"]]
139 assert "LC2_Cal.CH1.force_kg_tared" in names
140 assert "LC2_Cal.CH2.force_kg_tared" not in names
147 comps = [{
"name":
"LC2_Cal.CH1.force_kg",
"entity":
"LC2_Cal.CH1",
"field":
"force_kg"}]
148 before = run_config.annotate({
"components": list(comps)}, run[
"id"])
149 assert "LC2_Cal.CH1.force_kg_tared" not in [c[
"name"]
for c
in before[
"components"]]
152 {
"entity":
"LC2_Cal.CH1",
"event":
"set",
"offsetKg": 20.0,
"appliedAtMs": 150_000},
154 after = run_config.annotate({
"components": list(comps)}, run[
"id"])
155 assert "LC2_Cal.CH1.force_kg_tared" in [c[
"name"]
for c
in after[
"components"]]
161 assert export_cache.INDEX_VERSION == 3
168 p = run[
"elodin"] / run[
"id"] /
"lc_tare.jsonl"
170 json.dumps({
"entity":
"LC2_Cal.CH1",
"event":
"set",
171 "offsetKg": 20.0,
"appliedAtMs": 150_000}) +
"\n"
172 +
'{"entity":"LC2_Cal.CH1","event":"rec'
174 assert lc_tare.load(run[
"id"]) == {
"LC2_Cal.CH1": [(150.0, 20.0)]}
178 assert lc_tare.load(run[
"id"]) == {}
183 {
"entity":
"LC2_Cal.CH1",
"event":
"set",
"offsetKg":
None,
"appliedAtMs": 150_000},
185 assert lc_tare.load(run[
"id"]) == {}
191 from .naming
import classify
192 assert classify(
"LC2_Cal.CH1.force_kg_tared").unit == classify(
"LC2_Cal.CH1.force_kg").unit
test_a_torn_final_line_does_not_lose_the_earlier_ones(run)
test_a_sidecar_dropped_in_later_takes_effect_without_a_re_export(run)
test_offset_applies_only_after_the_tare_was_set(run)
write_sidecar(run, lines)
test_a_non_finite_offset_is_skipped(run)
test_a_clear_returns_the_trace_to_absolute(run)
test_the_absolute_series_is_never_touched(run)
test_a_missing_sidecar_is_empty_not_an_error(run)
test_a_channel_with_no_tare_gets_no_twin(run)
test_the_unit_matches_its_absolute_twin(run)
test_gross_and_tared_do_not_share_a_cache_entry(run)
write_gross(run, entity="LC2_Cal.CH1", times=(100.0, 200.0, 300.0), values=(50.0, 51.0, 52.0))
test_a_recal_line_moves_the_offset_again(run)
test_no_sidecar_means_no_tared_component(run)
test_index_version_did_not_move(run)