Coverage for tests/io/test_evt4.py: 100%
70 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-15 09:33 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-15 09:33 +0000
1"""EVT4 decoder tests.
3EVT4 is a 32-bit-word format modelled on OpenEB's ``evt4`` HAL decoder. Its CD,
4TIME_HIGH and EXT_TRIGGER words reuse EVT2's bit layout with distinct type codes,
5so the scalar path is validated against the proven EVT2 decoder. The format's
6novel mechanic is vectorised CD (CD_VEC_OFF=0xC / CD_VEC_ON=0xD): a base word
7carrying x/y/ts/polarity followed by a 32-bit validity mask, emitting one event
8per set bit at ``x = base_x + bit_index`` (same y/ts/polarity). No encoder emits
9CD_VEC, so it is exercised here with a hand-crafted word stream.
10"""
11import numpy as np
13from typing import Any
16# ---- EVT4 word crafting (see csrc/evt4.c for the bit layout) --------------
18def _time_high(t: int) -> int:
19 return (0xE << 28) | ((t >> 6) & 0x0FFFFFFF)
22def _cd(t: int, x: int, y: int, p: int) -> int:
23 # CD_OFF=0xA / CD_ON=0xB
24 return ((0xA | (p & 1)) << 28) | ((t & 0x3F) << 22) | ((x & 0x7FF) << 11) | (y & 0x7FF)
27def _cd_vec_base(t: int, base_x: int, y: int, p: int) -> int:
28 # CD_VEC_OFF=0xC / CD_VEC_ON=0xD; followed by a 32-bit mask word.
29 return ((0xC | (p & 1)) << 28) | ((t & 0x3F) << 22) | ((base_x & 0x7FF) << 11) | (y & 0x7FF)
32def _header_only(tmp_path: Any) -> Any:
33 """An EVT4 file with a valid header and no payload, ready to append raw words."""
34 from evutils.io import EventWriter
35 p = tmp_path / "evt4_raw.raw"
36 w = EventWriter(p, format="evt4")
37 w.init()
38 w.close()
39 return p
42def _append(p: Any, words: Any) -> None:
43 with open(p, "ab") as f:
44 f.write(np.asarray(words, dtype=np.uint32).tobytes())
47# ---- scalar path: EVT4 must decode identically to EVT2 --------------------
49def test_evt4_matches_evt2(tmp_path: Any, test_events: Any) -> None:
50 """Same events encoded as EVT2 and EVT4 must decode to identical arrays
51 (both formats share the CD / TIME_HIGH layout)."""
52 from evutils.io import EventWriter, EventReader
53 out = {}
54 for fmt in ("evt2", "evt4"):
55 p = tmp_path / f"{fmt}.raw"
56 with EventWriter(p, format=fmt) as w:
57 w.write(test_events)
58 with EventReader(p) as r:
59 out[fmt] = np.asarray(r.read_all())
60 assert np.array_equal(out["evt4"], test_events), "evt4 scalar round-trip differs from input"
61 assert np.array_equal(out["evt4"], out["evt2"]), "evt4 decode differs from evt2"
64# ---- vectorised CD: the novel mechanic, hand-crafted ----------------------
66def test_evt4_vector_cd(tmp_path: Any) -> None:
67 """A CD_VEC base + 32-bit mask emits one event per set bit at base_x+bit."""
68 from evutils.io import EventReader
69 p = _header_only(tmp_path)
71 t, base_x, y, pol = 5, 100, 50, 1
72 mask = (1 << 0) | (1 << 3) | (1 << 31) # bits 0, 3, 31 set
73 _append(p, [_time_high(t), _cd_vec_base(t, base_x, y, pol), mask])
75 with EventReader(p) as r:
76 out = np.asarray(r.read_all())
78 assert len(out) == 3, f"expected 3 events from the 3-bit mask, got {len(out)}"
79 assert np.array_equal(out["x"], [100, 103, 131]), "vector x offsets wrong"
80 assert np.array_equal(out["y"], [50, 50, 50]), "vector y not held constant"
81 assert np.array_equal(out["p"], [1, 1, 1]), "vector polarity wrong"
82 assert np.array_equal(out["t"], [5, 5, 5]), "vector timestamp wrong"
85def test_evt4_vector_empty_mask(tmp_path: Any) -> None:
86 """A CD_VEC with an all-zero mask emits nothing but still consumes the mask
87 word, so a following scalar CD decodes correctly."""
88 from evutils.io import EventReader
89 p = _header_only(tmp_path)
90 _append(p, [
91 _time_high(7),
92 _cd_vec_base(7, 200, 60, 0), 0x00000000, # empty mask -> no events
93 _cd(7, 300, 70, 1), # must still decode
94 ])
95 with EventReader(p) as r:
96 out = np.asarray(r.read_all())
97 assert len(out) == 1, f"empty mask should yield only the trailing CD, got {len(out)}"
98 assert (int(out["x"][0]), int(out["y"][0]), int(out["p"][0])) == (300, 70, 1)
101def test_evt4_vector_and_scalar_mixed(tmp_path: Any) -> None:
102 """Interleaved scalar CD, vector CD (both polarities) and TIME_HIGH decode
103 in order with correct timestamps."""
104 from evutils.io import EventReader
105 p = _header_only(tmp_path)
106 _append(p, [
107 _time_high(10),
108 _cd(10, 1, 2, 0), # scalar, p=0
109 _cd_vec_base(10, 500, 300, 1), (1 << 1) | (1 << 2), # x=501,502 p=1
110 _cd_vec_base(10, 8, 9, 0), (1 << 0), # x=8 p=0
111 _cd(10, 3, 4, 1), # scalar, p=1
112 ])
113 with EventReader(p) as r:
114 out = np.asarray(r.read_all())
115 assert np.array_equal(out["x"], [1, 501, 502, 8, 3])
116 assert np.array_equal(out["y"], [2, 300, 300, 9, 4])
117 assert np.array_equal(out["p"], [0, 1, 1, 0, 1])
118 assert np.array_equal(out["t"], [10, 10, 10, 10, 10])
121def test_evt4_vector_full_mask(tmp_path: Any) -> None:
122 """A fully-set 32-bit mask emits 32 contiguous events base_x .. base_x+31."""
123 from evutils.io import EventReader
124 p = _header_only(tmp_path)
125 base_x = 64
126 _append(p, [_time_high(3), _cd_vec_base(3, base_x, 11, 1), 0xFFFFFFFF])
127 with EventReader(p) as r:
128 out = np.asarray(r.read_all())
129 assert len(out) == 32
130 assert np.array_equal(out["x"], np.arange(base_x, base_x + 32))
131 assert np.all(out["y"] == 11) and np.all(out["p"] == 1) and np.all(out["t"] == 3)