Coverage for src/evutils/io/_aer.py: 83%

127 statements  

« prev     ^ index     » next       coverage.py v7.15.1, created at 2026-07-18 05:24 +0000

1"""Prophesee AER CD-event decoder/encoder. 

2 

3AER is a raw 32-bit-per-event encoding with **no header and no timestamps**: 

4``y[0:8]`` (9 bits), ``x[9:17]`` (9 bits), ``p[18]``. The 9-bit fields cap 

5coordinates at 512 (e.g. GenX320). Decoding uses the native 

6``AER_parse_chunk_soa``; encoding is vectorised numpy. 

7 

8Since the format carries no time information, the decoder's ``timestamps`` 

9parameter selects how the ``t`` column is generated: 

10 

11* ``"zero"`` (default) -- every event gets ``t = 0``; 

12* ``"sequential"`` -- ``t = t_start + i * t_step``, generated in the native 

13 parser and carried across chunks; 

14* an array -- user-provided timestamps, assigned positionally (event ``i`` in 

15 the stream gets ``timestamps[i]``). 

16""" 

17from __future__ import annotations 

18 

19import io 

20from datetime import datetime 

21 

22import numpy as np 

23 

24from ..types import EventArray, TriggerArray 

25from .common import EventDecoder, EventEncoder 

26from ._native_core import ( 

27 EventSoABuffers, 

28 TriggerSoABuffers, 

29 decode_all_soa, 

30 events_view, 

31 parse_step, 

32) 

33from ._native_aer import ( 

34 AER_TS_SEQUENTIAL, 

35 AER_TS_ZERO, 

36 AerInput, 

37 AerParser, 

38) 

39from ._source import ByteSource 

40 

41_EMPTY_EVENTS = EventArray.empty() 

42 

43class EventDecoder_AER(EventDecoder): 

44 """Decode raw AER streams into ``EventArray`` chunks. Since AER is designed 

45 for real-time streaming, it has no header and no timestamps; see the 

46 ``timestamps`` parameter for how the ``t`` column is generated. 

47 

48 Parameters 

49 ---------- 

50 source 

51 Byte source to read from. 

52 chunk_size 

53 Maximum number of events produced per :meth:`read_chunk` call (the 

54 native output-buffer capacity). Does not bound the file size. 

55 timestamps : {"zero", "sequential"} or array_like, default "zero" 

56 Timestamp generation mode: ``"zero"`` fills ``t = 0``, 

57 ``"sequential"`` fills ``t = t_start + i * t_step``, and an integer 

58 array assigns user-provided timestamps positionally (its length must 

59 cover every decoded event). 

60 t_start, t_step : int 

61 Start value and per-event increment for ``"sequential"`` mode. 

62 

63 References 

64 ---------- 

65 [1] Prophesee AER format: https://docs.prophesee.ai/stable/data/encoding_formats/aer.html 

66 

67 """ 

68 

69 #: AER is exactly one event per uint32 word, so the parser fills an output 

70 #: buffer to precisely its capacity -> eligible for EventReader's zero-copy 

71 #: n_events fast path (via parse_step). 

72 _exact_window = True 

73 

74 #: init() slurps the whole payload into memory (or mmaps it). 

75 _buffers_in_memory = True 

76 

77 def __init__(self, source: ByteSource, chunk_size: int = 1_000_000, 

78 timestamps: 'str | np.ndarray' = "zero", 

79 t_start: int = 0, t_step: int = 1): 

80 super().__init__(source, chunk_size) 

81 self._buf: bytes | bytearray | None = None 

82 self._words: "np.ndarray | None" = None # uint32 view of the payload (1 word / event) 

83 self._offset: int = 0 

84 self._parser: "Callable | None" = None 

85 self._events: "EventArray | None" = None 

86 self._triggers: "TriggerArray | None" = None 

87 

88 self._custom_ts: np.ndarray | None = None 

89 if isinstance(timestamps, str): 

90 modes = {"zero": AER_TS_ZERO, "sequential": AER_TS_SEQUENTIAL} 

91 if timestamps not in modes: 

92 raise ValueError( 

93 f"timestamps must be 'zero', 'sequential' or an array, got {timestamps!r}" 

94 ) 

95 self._ts_mode = modes[timestamps] 

96 else: 

97 ts = np.ascontiguousarray(timestamps, dtype=np.int64) 

98 if ts.ndim != 1: 

99 raise ValueError("custom timestamps must be a 1-D array") 

100 self._custom_ts = ts 

101 self._ts_mode = AER_TS_ZERO # parser fills 0; overwritten below 

102 self._t_start = int(t_start) 

103 self._t_step = int(t_step) 

104 self._n_decoded = 0 # stream position, for indexing the custom array 

105 

106 def init(self) -> None: 

107 """Initialize the AER reader. 

108 

109 Returns 

110 ------- 

111 None 

112 

113 """ 

114 if self._is_initialized: 

115 return 

116 

117 if self._source.mappable(): 

118 self._buf = self._source.buffer() 

119 else: 

120 self._buf = memoryview(self._source.read(-1)) 

121 

122 n_events = len(self._buf) // 4 # AER has no header, 4 bytes / event 

123 if n_events > 0: 

124 self._words = np.frombuffer(self._buf, dtype=np.uint32, count=n_events) 

125 else: 

126 self._words = np.empty(0, dtype=np.uint32) 

127 

128 if self._custom_ts is not None and len(self._custom_ts) < n_events: 

129 raise ValueError( 

130 f"custom timestamps array has {len(self._custom_ts)} entries, " 

131 f"but the stream contains {n_events} events" 

132 ) 

133 

134 self._offset = 0 

135 self._parser = AerParser(self._ts_mode, self._t_start, self._t_step) 

136 self._input_cls = AerInput 

137 self._word_dtype = np.uint32 

138 cap = int(self._chunk_size) 

139 self._events = EventSoABuffers(cap) 

140 self._triggers = TriggerSoABuffers(1) # AER has no triggers 

141 self._is_initialized = True 

142 

143 def _apply_custom_ts(self, t_out: np.ndarray) -> None: 

144 """Overwrite ``t_out`` (the decoded slice's t column, int64/uint64 view) 

145 with the next ``len(t_out)`` user-provided timestamps. 

146 """ 

147 assert self._custom_ts is not None 

148 n = len(t_out) 

149 t_out.view(np.int64)[:] = self._custom_ts[self._n_decoded:self._n_decoded + n] 

150 

151 def parse_step(self, events: EventSoABuffers, triggers: TriggerSoABuffers) -> int: 

152 """Run the parser once, appending into ``events``; advance the offset. 

153 See :meth:`EventDecoder_EVT.parse_step`. 

154 """ 

155 if not self._is_initialized: 

156 self.init() 

157 if self._words is None or self._offset >= len(self._words): 

158 self._eof = True 

159 return 0 

160 appended, self._offset = parse_step( 

161 self._words, self._offset, AerInput, self._parser, events, triggers, 

162 ) 

163 if appended and self._custom_ts is not None: 

164 self._apply_custom_ts(events.t[events.size - appended:events.size]) 

165 self._n_decoded += appended 

166 if self._offset >= len(self._words): 

167 self._eof = True 

168 return appended 

169 

170 def read_chunk(self, delta_t_hint: int | None = None, 

171 n_events_hint: int | None = None) -> EventArray: 

172 if not self._is_initialized: 

173 self.init() 

174 

175 if self._words is None or self._offset >= len(self._words): 

176 self._eof = True 

177 return _EMPTY_EVENTS 

178 

179 ev, tr = self._events, self._triggers 

180 ev.reset() 

181 tr.reset() 

182 appended = 0 

183 while appended == 0 and self._offset < len(self._words): 

184 appended = self.parse_step(ev, tr) 

185 

186 n = ev.size 

187 if n == 0: 

188 return _EMPTY_EVENTS 

189 # Zero-copy view (valid until the next read_chunk); see EVT decoder. 

190 return events_view(ev) 

191 

192 def read_all(self) -> EventArray: 

193 """Decode the whole remaining payload into one buffer (no per-chunk copy).""" 

194 if not self._is_initialized: 

195 self.init() 

196 if self._words is None or self._offset >= len(self._words): 

197 self._eof = True 

198 return _EMPTY_EVENTS 

199 start = self._n_decoded 

200 # Exactly one event per uint32 word. 

201 out, self._offset = decode_all_soa( 

202 self._words, self._offset, AerInput, self._parser, 

203 est_events_per_word=1.0, 

204 ) 

205 self._n_decoded = start + len(out) 

206 if self._custom_ts is not None and len(out) > 0: 

207 out.t[:] = self._custom_ts[start:start + len(out)] 

208 self._eof = True 

209 return out 

210 

211 def reset(self) -> None: 

212 """Reset the AER reader to the beginning. 

213 

214 Returns 

215 ------- 

216 None 

217 

218 """ 

219 self._offset = 0 

220 self._n_decoded = 0 

221 self._eof = False 

222 if self._parser is not None: 

223 self._parser.reset() 

224 

225 def tell(self) -> int: 

226 """Get the current byte offset. 

227 

228 Returns 

229 ------- 

230 int 

231 Current byte offset. 

232 

233 """ 

234 return self._offset * 4 

235 

236 def close(self) -> None: 

237 """Close the AER reader. 

238 

239 Returns 

240 ------- 

241 None 

242 

243 """ 

244 self._words = None 

245 self._buf = None 

246 

247class EventEncoder_AER(EventEncoder): 

248 """Encode events into a raw AER stream. Since AER is designed for real-time 

249 streaming, it has no header and no timestamps. 

250 Timestamps are dropped and coordinates are masked to 9 bits (values >= 512 

251 are truncated), per the AER encoding. 

252 

253 Parameters 

254 ---------- 

255 writable 

256 Destination stream to write to. 

257 width, height : int 

258 Frame geometry written into the header. 

259 dt : datetime, optional 

260 No effect, since AER has no timestamps. 

261 

262 References 

263 ---------- 

264 [1] Prophesee AER format: https://docs.prophesee.ai/stable/data/encoding_formats/aer.html 

265 

266 """ 

267 

268 def __init__(self, writable: io.BufferedWriter, width: int = 512, height: int = 512, dt: datetime | None = None): 

269 super().__init__(writable, width, height, dt) 

270 

271 def init(self) -> None: 

272 """Initialize the AER writer. 

273 

274 Returns 

275 ------- 

276 None 

277 

278 """ 

279 self._is_initialized = True # AER has no header 

280 

281 def write(self, events: 'np.ndarray | EventArray', triggers: 'np.ndarray | TriggerArray | None' = None) -> int: 

282 """Write events to the AER file. 

283 

284 Parameters 

285 ---------- 

286 events : np.ndarray or EventArray 

287 Array of events to write. 

288 

289 Returns 

290 ------- 

291 int 

292 Number of written events. 

293 

294 """ 

295 if not self._is_initialized: 

296 self.init() 

297 

298 if isinstance(events, EventArray): 

299 x, y, p = events.x, events.y, events.p 

300 else: 

301 x, y, p = events["x"], events["y"], events["p"] 

302 

303 out = ( 

304 (y.astype(np.uint32) & np.uint32(0x1FF)) 

305 | ((x.astype(np.uint32) & np.uint32(0x1FF)) << np.uint32(9)) 

306 | ((p.astype(np.uint32) & np.uint32(0x1)) << np.uint32(18)) 

307 ) 

308 self._fd.write(out.astype(np.uint32).tobytes()) 

309 self._n_written_events += len(out) 

310 return len(out)