Coverage for tests/io/test_roundtrip_matrix.py: 100%

119 statements  

« prev     ^ index     » next       coverage.py v7.15.1, created at 2026-07-15 09:33 +0000

1"""Cross-format round-trip matrix. 

2 

3Every read+write format goes through the same battery: bulk round-trip, 

4chunked writing, windowed reading (``n_events`` and ``delta_t``), reset, 

5iteration, empty files and timestamp normalization. Format-specific edge 

6cases (timestamp wraps, container layouts, ...) live in the per-format test 

7modules. 

8""" 

9import numpy as np 

10import pytest 

11 

12from typing import Any 

13from evutils.io import EventReader, EventWriter 

14from evutils.types import Event_dtype 

15 

16@pytest.fixture(autouse=True) 

17def skip_missing_deps(request: Any) -> None: 

18 if "fmt" in request.fixturenames: 

19 fmt = request.getfixturevalue("fmt") 

20 

21 if fmt == "hdf5": 

22 pytest.importorskip("h5py") 

23 pytest.importorskip("hdf5plugin") 

24 

25# name -> (suffix, writer kwargs, capabilities) 

26FORMATS = { 

27 "evt3": (".raw", {"format": "evt3"}, {}), 

28 "evt21": (".raw", {"format": "evt21"}, {}), 

29 "evt2": (".raw", {"format": "evt2"}, {}), 

30 "dat": (".dat", {}, {}), 

31 "aer": (".aer", {}, {"lossy_t": True, "coord_max": 512}), 

32 "npz": (".npz", {}, {}), 

33 "hdf5": (".h5", {}, {}), 

34 "csv": (".csv", {}, {}), 

35} 

36 

37PARAMS = sorted(FORMATS) 

38 

39 

40def make_events(n: int=5000, coord_max: Any=None, seed: int=42, t_max: int=100_000) -> Any: 

41 rng = np.random.default_rng(seed) 

42 ev = np.zeros(n, dtype=Event_dtype) 

43 ev["t"] = np.sort(rng.integers(0, t_max, n)) 

44 ev["x"] = rng.integers(0, coord_max or 1280, n) 

45 ev["y"] = rng.integers(0, coord_max or 720, n) 

46 ev["p"] = rng.integers(0, 2, n) 

47 return ev 

48 

49 

50def expected(ev: Any, caps: Any) -> Any: 

51 """The array a lossless read should return (AER drops timestamps).""" 

52 if caps.get("lossy_t"): 

53 ev = ev.copy() 

54 ev["t"] = 0 

55 return ev 

56 

57 

58def write_file(tmp_path: Any, fmt: str, ev: Any, n_chunks: int=1) -> Any: 

59 suffix, kwargs, _ = FORMATS[fmt] 

60 p = tmp_path / f"events_{fmt}{suffix}" 

61 with EventWriter(p, **kwargs) as w: # type: ignore 

62 for part in np.array_split(ev, n_chunks): 

63 w.write(part) 

64 return p 

65 

66 

67def assert_events_equal(out: Any, ref: Any, context: str="") -> None: 

68 out = np.asarray(out) 

69 assert len(out) == len(ref), f"{context}: length {len(out)} != {len(ref)}" 

70 for f in ("t", "x", "y", "p"): 

71 assert np.array_equal(out[f], ref[f]), f"{context}: field {f!r} differs" 

72 

73 

74@pytest.mark.parametrize("fmt", PARAMS) 

75def test_bulk_roundtrip(tmp_path: Any, fmt: str) -> None: 

76 caps = FORMATS[fmt][2] 

77 ev = make_events(coord_max=caps.get("coord_max")) 

78 p = write_file(tmp_path, fmt, ev) 

79 with EventReader(p) as r: 

80 assert_events_equal(r.read_all(), expected(ev, caps), fmt) 

81 

82 

83@pytest.mark.parametrize("fmt", PARAMS) 

84def test_chunked_write_roundtrip(tmp_path: Any, fmt: str) -> None: 

85 """Writing in many small chunks must be byte-equivalent to one bulk write.""" 

86 caps = FORMATS[fmt][2] 

87 ev = make_events(coord_max=caps.get("coord_max")) 

88 p = write_file(tmp_path, fmt, ev, n_chunks=13) 

89 with EventReader(p) as r: 

90 assert_events_equal(r.read_all(), expected(ev, caps), fmt) 

91 

92 

93@pytest.mark.parametrize("fmt", PARAMS) 

94def test_windowed_n_events(tmp_path: Any, fmt: str) -> None: 

95 caps = FORMATS[fmt][2] 

96 ev = make_events(coord_max=caps.get("coord_max")) 

97 p = write_file(tmp_path, fmt, ev) 

98 with EventReader(p, n_events=137) as r: 

99 chunks = [np.asarray(c) for c in r] 

100 assert all(len(c) <= 137 for c in chunks) 

101 assert_events_equal(np.concatenate(chunks), expected(ev, caps), fmt) 

102 

103 

104@pytest.mark.parametrize("fmt", PARAMS) 

105def test_windowed_delta_t(tmp_path: Any, fmt: str) -> None: 

106 caps = FORMATS[fmt][2] 

107 if caps.get("lossy_t"): 

108 pytest.skip("format has no timestamps") 

109 ev = make_events() 

110 p = write_file(tmp_path, fmt, ev) 

111 with EventReader(p, delta_t=1000) as r: 

112 chunks = [np.asarray(c) for c in r] 

113 for c in chunks: 

114 if len(c) > 1: 

115 assert int(c["t"].max()) - int(c["t"].min()) <= 1000 

116 assert_events_equal(np.concatenate(chunks), ev, fmt) 

117 

118 

119@pytest.mark.parametrize("fmt", PARAMS) 

120def test_reset(tmp_path: Any, fmt: str) -> None: 

121 caps = FORMATS[fmt][2] 

122 ev = make_events(coord_max=caps.get("coord_max")) 

123 p = write_file(tmp_path, fmt, ev) 

124 with EventReader(p) as r: 

125 first = np.asarray(r.read_all()).copy() 

126 r.reset() 

127 second = np.asarray(r.read_all()) 

128 assert_events_equal(second, first, fmt) 

129 

130 

131@pytest.mark.parametrize("fmt", PARAMS) 

132def test_empty_write_then_read(tmp_path: Any, fmt: str) -> None: 

133 """A writer that never receives events must still produce a readable, 

134 zero-event file.""" 

135 suffix, kwargs, _ = FORMATS[fmt] 

136 p = tmp_path / f"empty{suffix}" 

137 with EventWriter(p, **kwargs) as w: # type: ignore 

138 w.write(np.zeros(0, dtype=Event_dtype)) 

139 with EventReader(p) as r: 

140 out = r.read_all() 

141 assert not isinstance(out, tuple) 

142 assert len(out) == 0 

143 # The empty columns keep their canonical dtypes. 

144 assert out["t"].dtype == np.int64 

145 assert out["x"].dtype == np.uint16 

146 

147 

148@pytest.mark.parametrize("fmt", PARAMS) 

149def test_read_after_eof_is_empty(tmp_path: Any, fmt: str) -> None: 

150 caps = FORMATS[fmt][2] 

151 ev = make_events(n=100, coord_max=caps.get("coord_max")) 

152 p = write_file(tmp_path, fmt, ev) 

153 with EventReader(p) as r: 

154 r.read_all() 

155 assert r.is_eof() 

156 assert len(r.read_all()) == 0 

157 

158 

159@pytest.mark.parametrize("fmt", PARAMS) 

160def test_normalize_ts(tmp_path: Any, fmt: str) -> None: 

161 caps = FORMATS[fmt][2] 

162 if caps.get("lossy_t"): 

163 pytest.skip("format has no timestamps") 

164 ev = make_events() 

165 ev["t"] += 5000 # ensure a non-zero first timestamp 

166 p = write_file(tmp_path, fmt, ev) 

167 with EventReader(p, normalize_ts=True) as r: 

168 out = r.read_all() 

169 assert not isinstance(out, tuple) 

170 assert int(out["t"][0]) == 0 

171 assert np.array_equal(np.diff(out["t"]), np.diff(ev["t"].astype(np.int64)))