1"""Unit tests for the data-shaping logic (no elodin-db / real run needed).
3Run: cd webviewer && .venv/bin/python -m pytest backend/test_series.py -q
10from .
import config, export_cache, series
11from .naming
import classify
15 t = np.arange(10000, dtype=float)
19 dt, dv = series.decimate_minmax(t, v, 200)
22 assert dv.max() == 999.0
23 assert dv.min() == -999.0
25 assert np.all(np.diff(dt) >= 0)
30 t = np.arange(100000, dtype=float)
35 dt, dv = series.decimate_discrete(t, v, 4000)
37 assert dt.tolist() == [0.0, 25000.0, 50000.0, 75000.0, 99999.0]
38 assert dv.tolist() == [0.0, 1.0, 2.0, 1.0, 1.0]
43 t = np.arange(20000, dtype=float)
44 v = (t % 2).astype(float)
45 dt, _ = series.decimate_discrete(t, v, 1000)
46 assert len(dt) <= 1000
50 t = np.arange(50, dtype=float)
52 dt, dv = series.decimate_minmax(t, v, 4000)
53 assert np.array_equal(dt, t)
and np.array_equal(dv, v)
57 t = np.arange(0, 100, dtype=float)
59 st, sv = series._slice(t, v, 10.0, 20.0)
60 assert st[0] == 10.0
and st[-1] == 20.0
64 c = classify(
"PT_Cal.Ox_Upstream.pressure_psi")
65 assert c.family ==
"PT_Cal" and c.field ==
"pressure_psi"
66 assert c.unit ==
"psi" and not c.discrete
and c.primary
68 d = classify(
"ACT.Fuel_Fill_Press.actuator_state")
69 assert d.discrete
and d.family ==
"ACT"
71 h = classify(
"1399096562196883797")
72 assert h.family ==
"Other" and not h.primary
77 raw = classify(
"T71.AIN1.volts")
78 assert raw.family ==
"T7" and raw.unit ==
"V" and raw.primary
79 cal = classify(
"T71_Cal.AIN1.pressure_psi")
80 assert cal.family ==
"T7_Cal" and cal.unit ==
"psi"
81 code = classify(
"T71_Cal.AIN1.raw_uv")
82 assert code.unit ==
"µV" and not code.primary
83 assert classify(
"T71.CJC.temperature_c").unit ==
"°C"
85 assert classify(
"PT1.CH1.raw_adc_counts").family ==
"PT"
90 def fake_load(_run, name, _src="sensor"):
92 return np.array([0.0, 2.0]), np.array([10.0, 20.0])
93 return np.array([1.0]), np.array([1.0])
95 monkeypatch.setattr(series,
"load_series", fake_load)
98 lambda n: type(
"C", (), {
"discrete": n ==
"disc",
"entity": n})(),
100 text =
"".join(series.wide_csv_rows(
"r", [
"cont",
"disc"],
None,
None))
101 lines = text.strip().
split(
"\n")
102 assert lines[0] ==
"time,cont,disc"
103 assert lines[1] ==
",,"
104 body = [ln.split(
",")
for ln
in lines[2:]]
105 times = [float(r[0])
for r
in body]
106 assert times == [0.0, 1.0, 2.0]
108 disc_col = [r[2]
for r
in body]
109 assert disc_col[0] ==
"" and disc_col[1] ==
"1" and disc_col[2] ==
"1"
113 """The second header row names what each column IS. The component name stays the
114 column key (every analysis script keys on it); the role rides
115 underneath, blank where the run's config names nothing."""
116 monkeypatch.setattr(config,
"ELODIN_DIR", tmp_path)
117 (tmp_path /
"daq_20260902_120000.toml").write_text(
118 '[boards.pt_board]\ntype = "PT"\nboard_id = 21\n'
119 '\n[sensor_roles_pt_board]\n"Ox Upstream" = 5\n'
122 series,
"load_series",
lambda _r, _n, _s=
"sensor": (np.array([0.0]), np.array([1.0]))
125 series.wide_csv_rows(
126 "daq_20260902_120000",
127 [
"PT1.CH5.pressure_psi",
"PT1.CH9.pressure_psi"],
132 header, roles = text.split(
"\n")[:2]
133 assert header ==
"time,PT1.CH5.pressure_psi,PT1.CH9.pressure_psi"
134 assert roles ==
",Ox Upstream,"
135 assert header.count(
",") == roles.count(
",")
143 """A one-entity run on disk, with both clocks. Caches cleared so each test's fixture
144 data is actually read (load_series/sensor_clock memoise on run id)."""
145 monkeypatch.setattr(export_cache,
"cache_dir",
lambda run_id: tmp_path)
146 series.load_series.cache_clear()
147 series.sensor_clock.cache_clear()
152 """One flattened-export parquet: the DB's write time plus one value column."""
155 "time": pd.to_datetime(np.asarray(db_times) * 1e9, unit=
"ns"),
156 name.rsplit(
".", 1)[-1]: values,
158 ).to_parquet(dir_ / f
"{name}.parquet")
163BURSTY_DB = [1000.0, 1000.00004, 1000.00008, 1000.1, 1000.10004, 1000.10008]
164EVEN_SENSOR = [1000.0, 1000.01, 1000.02, 1000.1, 1000.11, 1000.12]
168 _write(clock_run,
"PT1.CH1.raw_adc_counts", BURSTY_DB, [1, 2, 3, 4, 5, 6])
169 _write(clock_run,
"PT1.CH1.timestamp_ns", BURSTY_DB, [t * 1e9
for t
in EVEN_SENSOR])
171 t, v = series.load_series(
"r",
"PT1.CH1.raw_adc_counts",
"sensor")
172 assert t.tolist() == pytest.approx(EVEN_SENSOR)
173 assert v.tolist() == [1, 2, 3, 4, 5, 6]
175 t_db, _ = series.load_series(
"r",
"PT1.CH1.raw_adc_counts",
"db")
176 assert t_db.tolist() == pytest.approx(BURSTY_DB)
180 """The sequencer and heartbeat router stamp steady_clock — nanoseconds since boot,
181 not since 1970. Taken literally that lands them 56 years off; but the clock itself is
182 good, so it is shifted onto the epoch by the median write latency. That keeps their
183 precise relative timing instead of falling back to the jittery DB write time."""
184 boot = (12.0, 12.01, 12.02, 12.1, 12.11, 12.12)
185 _write(clock_run,
"ACT_CMD.B2.CH1.actuator_state", BURSTY_DB, [0, 1, 0, 1, 0, 1])
186 _write(clock_run,
"ACT_CMD.B2.CH1.timestamp_ns", BURSTY_DB, [t * 1e9
for t
in boot])
188 arr, kind = series.sensor_clock(
"r",
"ACT_CMD.B2.CH1")
189 assert kind == series.SOURCE_MONOTONIC
191 t, _ = series.load_series(
"r",
"ACT_CMD.B2.CH1.actuator_state",
"sensor")
193 assert t[0] == pytest.approx(BURSTY_DB[0], abs=0.2)
196 assert np.diff(t).tolist() == pytest.approx(np.diff(boot).tolist())
200 """Write delay is one-sided — the DB cannot write a row before it was stamped — so the
201 offset estimate is the floor of the observed delays, and every re-anchored row must
202 end up at or before its own write time. A median estimate breaks this for about half
203 the rows, which then read as though the DB wrote them before they happened."""
204 boot = (12.0, 12.01, 12.02, 12.03, 12.04)
206 db = [1000.005, 1000.050, 1000.026, 1000.060, 1000.045]
207 _write(clock_run,
"ACT_CMD.B2.CH9.actuator_state", db, [0, 1, 0, 1, 0])
208 _write(clock_run,
"ACT_CMD.B2.CH9.timestamp_ns", db, [t * 1e9
for t
in boot])
210 t, _ = series.load_series(
"r",
"ACT_CMD.B2.CH9.actuator_state",
"sensor")
211 assert np.all(t <= np.sort(db) + 1e-9)
212 assert np.diff(t).tolist() == pytest.approx(np.diff(boot).tolist())
218 _write(clock_run,
"SEQUENCER.state.current_state", [1000.0], [1])
219 _write(clock_run,
"SEQUENCER.state.timestamp_ns", [1000.0], [8.0 * 1e9])
220 t, _ = series.load_series(
"r",
"SEQUENCER.state.current_state",
"sensor")
221 assert t.tolist() == pytest.approx([1000.0])
227 _write(clock_run,
"PT1.CH9.raw_adc_counts", BURSTY_DB, [1, 2, 3, 4, 5, 6])
228 _write(clock_run,
"PT1.CH9.timestamp_ns", BURSTY_DB, [t * 1e9
for t
in EVEN_SENSOR])
229 arr, kind = series.sensor_clock(
"r",
"PT1.CH9")
230 assert kind == series.SOURCE_SENSOR
231 assert arr.tolist() == pytest.approx(EVEN_SENSOR)
235 _write(clock_run,
"PT_Cal.Ox_Upstream.pressure_psi", BURSTY_DB, [1, 2, 3, 4, 5, 6])
236 t, _ = series.load_series(
"r",
"PT_Cal.Ox_Upstream.pressure_psi",
"sensor")
237 assert t.tolist() == pytest.approx(BURSTY_DB)
243 _write(clock_run,
"PT1.CH2.raw_adc_counts", BURSTY_DB, [1, 2, 3, 4, 5, 6])
244 _write(clock_run,
"PT1.CH2.timestamp_ns", BURSTY_DB[:4], [t * 1e9
for t
in EVEN_SENSOR[:4]])
245 t, _ = series.load_series(
"r",
"PT1.CH2.raw_adc_counts",
"sensor")
246 assert t.tolist() == pytest.approx(BURSTY_DB)
252 _write(clock_run,
"PT1.CH3.raw_adc_counts", [1000.0, 1000.1, 1000.2], [10, 20, 30])
253 _write(clock_run,
"PT1.CH3.timestamp_ns", [1000.0, 1000.1, 1000.2],
254 [1000.2e9, 1000.0e9, 1000.1e9])
255 t, v = series.load_series(
"r",
"PT1.CH3.raw_adc_counts",
"sensor")
256 assert np.all(np.diff(t) >= 0)
257 assert v.tolist() == [20, 30, 10]
261 """The run extent is a union across entities, each on its own clock. Command and
262 heartbeat channels start at boot, ~3.7 s before the first sensor packet on a real
263 run; measuring only the sensor-clocked ones clipped the start of every one of them."""
264 monkeypatch.setattr(export_cache,
"_db_size_bytes",
lambda _r:
None)
266 _write(clock_run,
"PT1.CH1.raw_adc_counts", [1010.0, 1012.0], [1, 2])
267 _write(clock_run,
"PT1.CH1.timestamp_ns", [1010.0, 1012.0], [1010.0e9, 1012.0e9])
268 _write(clock_run,
"ACT_CMD.B2.CH1.actuator_state", [1000.0, 1002.0], [0, 1])
269 _write(clock_run,
"ACT_CMD.B2.CH1.timestamp_ns", [1000.0, 1002.0], [5.0e9, 7.0e9])
271 idx = export_cache.build_index(
"r")
272 kinds = {c[
"entity"]: c[
"time_source"]
for c
in idx[
"components"]}
273 assert kinds[
"PT1.CH1"] ==
"sensor" and kinds[
"ACT_CMD.B2.CH1"] ==
"monotonic"
274 assert idx[
"n_reanchored"] == 2
and idx[
"n_db_only"] == 0
276 assert idx[
"sensor_t_min"] == pytest.approx(1000.0)
277 assert idx[
"sensor_t_max"] == pytest.approx(1012.0)
283 chunks = [
"x" * 100_000
for _
in range(5)]
285 got =
"".join(series.throttle(chunks, 1_000_000))
286 dt = _t.monotonic() - t0
287 assert len(got) == 500_000
292 chunks = [
"a",
"b",
"c"]
293 assert list(series.throttle(chunks, 0)) == chunks
static std::vector< string_t > split(const string_t &reference_string)
split the string input to reference tokens
test_slice_inclusive_bounds()
test_reanchored_rows_never_land_after_their_own_db_write(clock_run)
test_throttle_paces_throughput()
test_classify_labjack_t7()
test_wide_csv_union_and_ffill(monkeypatch)
test_classify_families_and_kinds()
test_missing_sibling_falls_back_to_db_time(clock_run)
test_decimate_noop_when_small()
test_index_extent_covers_channels_on_both_clocks(clock_run, monkeypatch)
test_throttle_disabled_is_passthrough()
test_decimate_discrete_keeps_only_transitions()
clock_run(tmp_path, monkeypatch)
test_wide_csv_role_row_aligns_with_the_columns(monkeypatch, tmp_path)
_write(dir_, name, db_times, values)
test_reanchoring_a_single_row_degrades_to_the_db_time(clock_run)
test_an_epoch_clock_is_not_shifted(clock_run)
test_decimate_preserves_spikes_and_bounds()
test_row_count_mismatch_falls_back_rather_than_mispairing(clock_run)
test_decimate_discrete_caps_pathological()
test_sensor_clock_replaces_the_bursty_db_write_time(clock_run)
test_boot_relative_clock_is_reanchored_not_discarded(clock_run)
test_series_are_sorted_by_whichever_clock_is_in_use(clock_run)