Coverage for src/evutils/io/_event_reader.py: 87%

294 statements  

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

1"""Event reader module. 

2 

3Provides the `EventReader` class for reading and decoding event data  

4from a file or byte stream. 

5""" 

6 

7import io 

8import time 

9from pathlib import Path 

10from typing import Any 

11 

12import numpy as np 

13 

14from ..types import EventArray, TriggerArray 

15 

16from . import decoders as ev_decoders 

17from ._prefetch import PrefetchIterator 

18from ._source import ByteSource, make_source 

19from .buffer import EventAccumulator 

20 

21 

22class EventReader(): 

23 """Class for reading and automatically decoding and slicing events from a file or stream. 

24 

25 The reader supports different modes of operation, including reading a fixed number of events, reading events within a time window, or a combination of both.  

26 The file_decoder is chosen automatically based on the file format or can be supplied explicitly. 

27 

28 Parameters 

29 ---------- 

30 file: Path or str or io.BufferedReader or bytes or ByteSource 

31 Path to the data file or a readable stream or bytes or a ByteSource 

32 delta_t: int or optional 

33 Time window in microseconds, by default None 

34 n_events: int or None 

35 Number of events to read in a chunk, by default None 

36 max_events: int, default=10_000_000 

37 Maximum number of events to read at once 

38 mode: {'delta_t', 'n_events', 'mixed', 'all', 'auto'}, default 'auto' 

39 Mode of operation ```["delta_t", "n_events", "mixed", "all", "auto"]``` 

40 start_ts: int, default=0 

41 Start timestamp offset for the events, by default 0 (start of the file) 

42 normalize_ts: bool, default=False 

43 Normalize timestamps to start from zero 

44 max_time: int, default=1_000_000_000_000 

45 Maximum timestamp to read 

46 width: int or None 

47 Width of the frame, by default infered from the file 

48 height: int or None 

49 Height of the frame, by default infered from the file 

50 ext_trigger: bool, default=False 

51 Whether to read external trigger events, by default False 

52 async_read: bool, default=False 

53 Decode ahead in a background thread while iterating: the next window 

54 is parsed while the caller processes the current one (the native 

55 parsers release the GIL). Helps whenever per-chunk processing takes 

56 meaningful time (numpy pipelines, GPU inference -- the read becomes 

57 essentially free); can slightly hurt when the processing already 

58 saturates memory bandwidth. Affects iteration only; ``read()`` / 

59 ``read_all()`` stay synchronous and raise while an asynchronous 

60 iterator is active. 

61 real_time: bool, default=False 

62 Pace :meth:`read` and iteration to the recording's own timeline: each 

63 chunk is released only once the wall-clock time since the first chunk 

64 matches the event time it covers, so the stream plays back as if live. 

65 Pacing is anchored to an absolute start time, so time spent processing 

66 a chunk is absorbed automatically (no drift); if decoding or 

67 processing falls behind, chunks are released immediately with no added 

68 delay. ``read_all()`` is never paced. Combine with ``async_read=True`` 

69 to decode ahead while waiting out the pacing delay. 

70 playback_speed: float, default=1.0 

71 Playback rate multiplier for ``real_time`` mode: ``2.0`` plays twice 

72 as fast as the recording, ``0.5`` at half speed. Ignored when 

73 ``real_time=False``. 

74 max_gap: float or None, default=1.0 

75 Longest idle stretch, in real seconds, that ``real_time`` pacing will 

76 sleep through. If a recording goes silent for longer (e.g. a 

77 late-starting stream, or a pause mid-recording), the pacer skips the 

78 dead air and resumes immediately instead of blocking on one long 

79 sleep. ``None`` disables the skip (strict real time). Ignored when 

80 ``real_time=False``. 

81 file_decoder: ev_decoders.EventDecoder or type[ev_decoders.EventDecoder] or None, default=None 

82 File decoder to use, by default None - automatic 

83 **kwargs 

84 Additional arguments to pass to the file decoder 

85 

86 Raises 

87 ------ 

88 ValueError 

89 If the mode is not supported or if the delta_t or n_events are not specified when needed 

90 

91 Examples 

92 -------- 

93 >>> from evutils.io import EventReader 

94 >>> # Read events in chunks of 10ms (10,000 microseconds) 

95 >>> reader = EventReader(b"% format EVT3;height=720;width=1280\\n% end\\n", delta_t=10000) 

96 >>> for events in reader: 

97 ... # Access fields directly 

98 ... x, y, p, t = events.x, events.y, events.p, events.t 

99 ... print(f"Processed {len(events)} events") 

100 Processed 0 events 

101 

102 TODO: 

103 Architectural Refactor Roadmap: 

104 Decouple `EventReader`'s monolithic design by extracting the core byte-to-array decoding into  

105 a native `EventStreamer`. The slicing logic (`mode="delta_t"`, `mode="n_events"`, etc.) should  

106 be moved to composable pipeline generators in `evutils.chunking`. `EventReader` will then  

107 become a simple Façade that dynamically assembles these pipeline generators to maintain  

108 this exact backward-compatible API, while allowing power-users to compose custom pipelines  

109 (e.g., slicing by external trigger boundaries). 

110 """ 

111 

112 READING_MODES = ["delta_t", "n_events", "mixed", "all", "auto"] 

113 DEFAULT_N_EVENTS = 1_000_000 

114 DEFAULT_DELTA_T = 10_000 

115 def __init__(self, file: Path | str | io.BufferedReader | bytes | ByteSource, 

116 delta_t:int|None=None, 

117 n_events:int|None=None, 

118 mode:str="auto", 

119 start_ts:int=0, 

120 normalize_ts: bool=False, 

121 max_time:int=1_000_000_000_000, 

122 max_events:int=10_000_000, 

123 width:int | None=None, height:int | None=None, 

124 ext_trigger: bool=False, 

125 async_read: bool=False, 

126 real_time: bool=False, 

127 playback_speed: float=1.0, 

128 max_gap: float | None=1.0, 

129 file_decoder: ev_decoders.EventDecoder | type[ev_decoders.EventDecoder] | None = None, 

130 **kwargs: Any) -> None: 

131 

132 # Remember the path (if any) for repr / reset semantics. 

133 self._file_name: Path | None = Path(file) if isinstance(file, (str, Path)) else None 

134 self._read_external_triggers = ext_trigger 

135 

136 # 1. Normalise the input into a ByteSource (path | stream | bytes | 

137 # BytesIO | ByteSource -> ByteSource). Regular files are memory-mapped. 

138 self._source: ByteSource = make_source(file) 

139 

140 # 2. Resolve the decoder and launch it: 

141 # explicit instance > explicit class > heuristic (extension, then 

142 # content sniffing of the source). 

143 if isinstance(file_decoder, ev_decoders.EventDecoder): 

144 self._file_decoder = file_decoder 

145 else: 

146 decoder_cls = file_decoder or ev_decoders.resolve_decoder_cls(self._source) 

147 self._file_decoder = decoder_cls(self._source, **kwargs) 

148 

149 self._file_decoder.read_external_triggers = self._read_external_triggers 

150 if self._read_external_triggers and not getattr(self._file_decoder, 'SUPPORTS_EXT_TRIGGERS', False): 

151 import warnings 

152 warnings.warn(f"{self._file_decoder.__class__.__name__} does not support reading external triggers.") 

153 

154 # This will now be io.BufferedReader 

155 self._eof = False 

156 

157 # If not defined explicitly, the width and height are fetch from the file (not all formats support this) 

158 self._width = width 

159 self._height = height 

160 self._start_ts = start_ts # Offset to start reading events. 0 is start of file 

161 self._first_ts = 0 # First timestamp in the file, used for normalization 

162 self._current_ts = self._first_ts 

163 self._normalize_ts = normalize_ts # Normalize timestamps to start from zero 

164 

165 

166 # Validate the parameters 

167 if mode not in EventReader.READING_MODES: 

168 raise ValueError(f"Mode {mode} not supported. Supported modes are: {EventReader.READING_MODES}") 

169 

170 

171 self._mode = mode.lower() 

172 

173 # if mode is auto, we will try to infer the mode from the parameters 

174 if self._mode == "auto": 

175 # If both delta_t and n_events are specified, we will use mixed mode 

176 if delta_t is not None and n_events is not None: 

177 self._mode = "mixed" 

178 

179 # If only one of the parameters is specified, we will use that mode, the other will be set to the maximum 

180 elif delta_t is not None: 

181 self._mode = "delta_t" 

182 n_events = max_events 

183 elif n_events is not None: 

184 self._mode = "n_events" 

185 delta_t = max_time 

186 else: 

187 # If none of the parameters are specified, we will use the default Values 

188 self._mode = "mixed" 

189 delta_t = self.DEFAULT_DELTA_T 

190 n_events = self.DEFAULT_N_EVENTS 

191 

192 # If the mode is not auto, we will check if the parameters are specified 

193 elif self._mode == "delta_t": 

194 if delta_t is None: 

195 raise ValueError("delta_t must be specified") 

196 n_events = max_events 

197 elif self._mode == "n_events": 

198 if n_events is None: 

199 raise ValueError("n_events must be specified") 

200 delta_t = max_time 

201 elif self._mode == "mixed": 

202 if delta_t is None: 

203 delta_t = self.DEFAULT_DELTA_T 

204 if n_events is None: 

205 n_events = self.DEFAULT_N_EVENTS 

206 

207 elif self._mode == "all": 

208 delta_t = max_time 

209 n_events = max_events 

210 

211 

212 # Validate the parameters 

213 if delta_t is None: 

214 delta_t = self.DEFAULT_DELTA_T 

215 if n_events is None: 

216 n_events = self.DEFAULT_N_EVENTS 

217 

218 if not isinstance(delta_t, int): 

219 raise TypeError("delta_t must be an integer") 

220 

221 if not isinstance(n_events, int): 

222 raise TypeError("n_events must be an integer") 

223 

224 

225 if delta_t <= 0: 

226 raise ValueError("delta_t must be positive") 

227 

228 if n_events <= 0: 

229 raise ValueError("n_events must be positive") 

230 

231 

232 # delta_t and n_events to read on each call 

233 self._delta_t = delta_t 

234 self._n_events = n_events 

235 

236 # Maximum number of events to read and maximum time to read in a chunk 

237 self._max_events = max_events if max_events < self._n_events else self._n_events 

238 self._max_time = max_time if max_time < self._delta_t else self._delta_t 

239 

240 self._is_initialized = False 

241 

242 # Windowed reads decode straight into this reused accumulator (no 

243 # intermediate copy); only the returned window is copied out. Allocated 

244 # lazily on first read() so the read_all() fast path never pays for it. 

245 # Granularity of a single decode step, and a native fast path flag. 

246 self._buffer: EventAccumulator | None = None 

247 self._step = 1 << 20 

248 # Hold the largest window we might be asked for, plus room for one decode 

249 # step of overshoot before the front is rotated out. (np.empty is lazy, so 

250 # a large capacity only costs pages actually written.) 

251 self._acc_capacity = max(self._n_events, max_events) + 2 * self._step 

252 self._native_fill = hasattr(self._file_decoder, "parse_step") 

253 

254 # Asynchronous iteration: when enabled, __iter__ decodes ahead in a 

255 # worker thread (see io/_prefetch.py). The active iterator owns the 

256 # decoder until it is exhausted or closed. 

257 self._async_read = async_read 

258 self._active_prefetch: PrefetchIterator | None = None 

259 

260 # Real-time playback: chunks are released against an absolute 

261 # wall-clock anchor set when the first chunk is delivered, so the 

262 # stream is paced like a live sensor (see _pace()). 

263 if not isinstance(playback_speed, (int, float)) or playback_speed <= 0: 

264 raise ValueError("playback_speed must be a positive number") 

265 self._real_time = real_time 

266 self._playback_speed = float(playback_speed) 

267 # Longest idle stretch (real seconds) the pacer will sleep through. If a 

268 # window would require a longer wait -- a silent gap in the recording, 

269 # e.g. a late-starting stream -- the pacer skips the dead air and 

270 # re-anchors so playback resumes immediately. None = strict real time. 

271 if max_gap is not None and (not isinstance(max_gap, (int, float)) or max_gap <= 0): 

272 raise ValueError("max_gap must be a positive number or None") 

273 self._max_gap = float(max_gap) if max_gap is not None else None 

274 self._pace_anchor: tuple[float, int] | None = None # (wall time, event ts) 

275 

276 self._n_read_events = 0 # Number of events read (not includeing events stored in buffer) 

277 

278 

279 

280 def init(self) -> None: 

281 """Initialize the reader, can be used explicitly or implicitly by the read method.""" 

282 if self._is_initialized: 

283 return 

284 self._file_decoder.init() 

285 self._is_initialized = True 

286 

287 def _check_no_active_prefetch(self) -> None: 

288 """Direct reads are not allowed while a prefetching iterator owns the 

289 decoder (its worker thread is reading concurrently).""" 

290 if self._active_prefetch is not None: 

291 raise RuntimeError( 

292 "An asynchronous iterator is active on this reader: exhaust or " 

293 "close() it before calling read()/read_all(), or iterate instead." 

294 ) 

295 

296 def _pull(self, acc: EventAccumulator, delta_t: int, n_events: int) -> int: 

297 """Pull more events into the accumulator, returning the number added 

298 (0 => end of stream). Native decoders decode straight into the 

299 accumulator's storage (no copy); others have their ``read_chunk`` output 

300 appended. 

301  

302 Parameters 

303 ---------- 

304 acc : EventAccumulator 

305 The accumulator to pull events into. 

306 delta_t : int 

307 The time window constraint for reading. 

308 n_events : int 

309 The number of events constraint for reading. 

310 

311 Returns 

312 ------- 

313 int 

314 The number of events added to the accumulator. 

315 

316 """ 

317 dec = self._file_decoder 

318 if self._native_fill: 

319 while True: 

320 if dec.is_eof(): 

321 return 0 

322 ev, tr = acc.prepare(self._step) 

323 added = dec.parse_step(ev, tr) # type: ignore[attr-defined] 

324 if added > 0: 

325 return int(added) 

326 if dec.is_eof(): 

327 return 0 

328 # else: consumed only state/timing words; step again. 

329 chunk = dec.read_chunk(delta_t, n_events) 

330 if isinstance(chunk, tuple): 

331 chunk, triggers = chunk 

332 else: 

333 triggers = None 

334 if len(chunk) == 0: 

335 return 0 

336 acc.append(chunk, triggers) 

337 return int(len(chunk)) 

338 

339 def read(self, delta_t:int|None=None, n_events:int|None=None) -> 'EventArray | tuple[EventArray, TriggerArray]': 

340 """Read events on the files based on the mode and the parameters. 

341 

342 Parameters 

343 ---------- 

344 delta_t 

345 Override the delta_t parameter, otherwise the default value is used from the constructor 

346 n_events 

347 Override the n_events parameter, otherwise the default value is used from the constructor 

348 

349 Returns 

350 ------- 

351 EventArray 

352 An array with the events 

353 

354 Examples 

355 -------- 

356 >>> reader = EventReader(b"% format EVT3;height=720;width=1280\\n% end\\n", delta_t=10000) 

357 >>> events = reader.read() 

358 >>> print(len(events)) 

359 0 

360 >>> # Override the time window for a single read 

361 >>> more_events = reader.read(delta_t=5000) 

362 

363 """ 

364 self._check_no_active_prefetch() 

365 out = self._read(delta_t, n_events) 

366 if self._real_time: 

367 self._pace(out) 

368 return out 

369 

370 def _pace(self, out: Any) -> None: 

371 """Sleep until the chunk's last event timestamp aligns with wall-clock 

372 playback time (anchored at the first delivered chunk). 

373 

374 The anchor is absolute, so time the caller spends between chunks is 

375 subtracted from the delay automatically; when the target time is 

376 already past (slow decode or slow consumer), no delay is added. 

377 """ 

378 ev = out[0] if isinstance(out, tuple) else out 

379 if len(ev) == 0: 

380 return 

381 t_last = int(ev.t[-1]) 

382 now = time.perf_counter() 

383 if self._pace_anchor is None: 

384 self._pace_anchor = (now, int(ev.t[0])) 

385 wall0, ts0 = self._pace_anchor 

386 target = wall0 + (t_last - ts0) / (1e6 * self._playback_speed) 

387 delay = target - now 

388 # A wait longer than max_gap means the recording went idle (e.g. a 

389 # silent lead-in before the action starts). Rather than stall on a 

390 # single multi-second sleep, skip the dead air: re-anchor to now so the 

391 # next window is paced relative to this one. 

392 if self._max_gap is not None and delay > self._max_gap: 

393 self._pace_anchor = (now, t_last) 

394 return 

395 if delay > 0: 

396 time.sleep(delay) 

397 

398 def _read(self, delta_t:int|None=None, n_events:int|None=None) -> Any: 

399 """Unguarded body of :meth:`read` (also driven by the prefetch worker).""" 

400 # If not initialized, initialize 

401 if not self._is_initialized: 

402 self.init() 

403 

404 # Allocate the staging accumulator on first use. 

405 if self._buffer is None: 

406 self._buffer = EventAccumulator(self._acc_capacity) 

407 acc = self._buffer 

408 

409 # "all" mode (without per-call overrides) means the whole remaining 

410 # stream: skip the window cutoffs entirely and drain to EOF, so files 

411 # exceeding max_time/max_events are still returned in full. 

412 drain = self._mode == "all" and delta_t is None and n_events is None 

413 

414 # Override the parameters if they are specified 

415 if delta_t is None: 

416 delta_t = self._delta_t 

417 if n_events is None: 

418 n_events = self._n_events 

419 

420 # Establish the first timestamp once, at the very start of the stream. 

421 if self._n_read_events == 0 and len(acc) == 0: 

422 if self._pull(acc, delta_t, n_events) == 0: 

423 self._eof = True 

424 if self._read_external_triggers: 

425 from ..types import TriggerArray 

426 return EventArray.empty(), TriggerArray.empty() 

427 return EventArray.empty() 

428 self._first_ts = int(acc.t_window()[0]) 

429 self._current_ts = self._first_ts 

430 

431 start_ts: int = self._current_ts 

432 end_ts: int = start_ts + delta_t # Final end_ts if we reach delta_t 

433 end_idx: int = len(acc) 

434 

435 tr_end_idx: int = acc._tr.size - acc._tr_start 

436 

437 # Gather events until we hit the n_events count, the delta_t time window, 

438 # or the end of the stream. Work directly on the SoA `t` column. 

439 # Both cutoffs are evaluated together: whichever falls earlier in the 

440 # stream wins (the buffer may hold far more than one window's worth). 

441 while True: 

442 t = acc.t_window() 

443 time_ready = not drain and len(t) > 0 and t[-1] >= end_ts 

444 count_ready = not drain and len(acc) > n_events 

445 

446 if time_ready or count_ready: 

447 time_idx = int(np.searchsorted(t, end_ts)) if time_ready else len(acc) + 1 

448 tr_t = acc.t_window_tr() 

449 if count_ready and time_idx > n_events: 

450 # n_events cutoff comes first 

451 end_idx = n_events 

452 self._current_ts = int(t[n_events]) 

453 tr_end_idx = int(np.searchsorted(tr_t, self._current_ts, side='left')) 

454 else: 

455 # delta_t cutoff comes first (ties go to the time window) 

456 end_idx = time_idx 

457 self._current_ts += delta_t 

458 tr_end_idx = int(np.searchsorted(tr_t, end_ts, side='left')) 

459 break 

460 

461 # Not enough buffered yet: pull more from the decoder. 

462 if self._pull(acc, delta_t, n_events) == 0: 

463 self._eof = True 

464 # Neither cutoff met, so the whole remaining buffer is the slice. 

465 end_idx = len(acc) 

466 tr_end_idx = acc._tr.size - acc._tr_start 

467 break 

468 

469 # Copy out the window (independent) and advance past it. 

470 output, output_tr = acc.slice_copy(end_idx, tr_end_idx) 

471 self._n_read_events += end_idx 

472 

473 if self._normalize_ts: 

474 # Normalize the timestamps to start from zero at start_ts. slice_copy 

475 # already returned an independent array, so this is safe in place. 

476 output.t -= self._first_ts - self._start_ts 

477 if len(output_tr) > 0: 

478 output_tr.t -= self._first_ts - self._start_ts 

479 

480 if self._read_external_triggers: 

481 return output, output_tr 

482 return output 

483 

484 def read_all(self) -> 'EventArray | tuple[EventArray, TriggerArray]': 

485 """Decode and return every remaining event at once. 

486 

487 Delegates to the decoder's :meth:`~evutils.io.common.EventDecoder.read_all`, 

488 which (for the native EVT/DAT/AER decoders) decodes the whole payload 

489 straight into a single output buffer -- no per-chunk copy, no final 

490 ``concatenate`` -- and hands the columns back as a zero-copy 

491 :class:`EventArray`. This bypasses the slicing ring buffer that 

492 :meth:`read` uses for ``delta_t``/``n_events`` windowing. 

493 

494 .. note:: 

495 This materialises every event in memory at once. For recordings too 

496 large to fit, iterate the reader (windowed :meth:`read`) instead. 

497 

498 Returns 

499 ------- 

500 EventArray 

501 All remaining events. 

502 

503 Examples 

504 -------- 

505 >>> reader = EventReader(b"% format EVT3;height=720;width=1280\\n% end\\n") 

506 >>> all_events = reader.read_all() 

507 >>> print(f"Total events: {len(all_events)}") 

508 Total events: 0 

509 

510 """ 

511 self._check_no_active_prefetch() 

512 if not self._is_initialized: 

513 self.init() 

514 

515 _out = self._file_decoder.read_all() 

516 if self._read_external_triggers: 

517 if isinstance(_out, tuple): 

518 out, out_tr = _out 

519 else: 

520 out = _out 

521 from ..types import TriggerArray 

522 out_tr = TriggerArray.empty() 

523 else: 

524 out = _out # type: ignore 

525 

526 # Prepend anything already buffered by prior read() calls (rare: only if 

527 # read() and read_all() are mixed on the same reader). 

528 if self._buffer is not None and (len(self._buffer) > 0 or self._buffer._tr.size - self._buffer._tr_start > 0): 

529 buffered, buffered_tr = self._buffer.slice_copy(len(self._buffer), self._buffer._tr.size - self._buffer._tr_start) 

530 if len(out) == 0: 

531 out = buffered 

532 elif len(buffered) > 0: 

533 out = EventArray( 

534 np.concatenate([buffered.t, out.t]), 

535 np.concatenate([buffered.x, out.x]), 

536 np.concatenate([buffered.y, out.y]), 

537 np.concatenate([buffered.p, out.p]), 

538 ) 

539 

540 if self._read_external_triggers: 

541 if len(out_tr) == 0: 

542 out_tr = buffered_tr 

543 elif len(buffered_tr) > 0: 

544 from ..types import TriggerArray 

545 out_tr = TriggerArray( 

546 np.concatenate([buffered_tr.t, out_tr.t]), 

547 np.concatenate([buffered_tr.p, out_tr.p]), 

548 np.concatenate([buffered_tr.id, out_tr.id]), 

549 ) 

550 

551 self._eof = True 

552 self._n_read_events += len(out) 

553 

554 if self._normalize_ts and len(out) > 0: 

555 # Capture the shift before modifying out.t, so triggers get the 

556 # same normalization. 

557 shift = int(out.t[0]) - self._start_ts 

558 out.t -= shift 

559 if self._read_external_triggers and len(out_tr) > 0: 

560 out_tr.t -= shift 

561 

562 if self._read_external_triggers: 

563 return out, out_tr 

564 return out 

565 

566 def reset(self) -> None: 

567 """Reset file reader back to the beginning of the file. 

568 

569 An active asynchronous iterator is cancelled first (its remaining 

570 buffered chunks are discarded). 

571 """ 

572 if self._active_prefetch is not None: 

573 self._active_prefetch.close() 

574 self._n_read_events = 0 

575 self._eof = False 

576 self._pace_anchor = None 

577 if self._buffer is not None: 

578 self._buffer.reset() 

579 self._file_decoder.reset() 

580 

581 def __enter__(self) -> "EventReader": 

582 return self 

583 

584 def is_eof(self) -> bool: 

585 """Check if the end of the file is reached. 

586 

587 Returns 

588 ------- 

589 bool 

590 True if the end of the file is reached, False otherwise 

591 

592 """ 

593 if not self._is_initialized: 

594 self.init() 

595 return self._eof and (self._buffer is None or len(self._buffer) == 0) 

596 

597 def close(self) -> None: 

598 """Close the reader and release resources (decoder buffer views, then the 

599 underlying byte source). Cancels any active asynchronous iterator first. 

600 """ 

601 if self._active_prefetch is not None: 

602 self._active_prefetch.close() 

603 # Drop decoder views (e.g. into an mmap) before closing the source. 

604 self._file_decoder.close() 

605 self._source.close() 

606 

607 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 

608 self.close() 

609 

610 def __repr__(self) -> str: 

611 if self._is_initialized: 

612 is_initialized_txt = "initialized" 

613 else: 

614 is_initialized_txt = "not initialized" 

615 src = self._file_name if self._file_name is not None else self._source.__class__.__name__ 

616 return f"{self.__class__.__name__}(source={src} - {is_initialized_txt}, delta_t={self._delta_t}, n_events={self._n_events}, mode={self._mode})" 

617 

618 def __len__(self) -> int: 

619 return self._n_read_events 

620 

621 def __iter__(self) -> Any: 

622 """Iterate over the events in the file. 

623 

624 With ``async_read=True`` the windows are decoded ahead in a background 

625 thread (bounded to a couple of windows), overlapping decode with the 

626 caller's per-chunk processing. Only one asynchronous iterator can be 

627 active per reader; direct :meth:`read` / :meth:`read_all` calls raise 

628 until it is exhausted or closed. 

629 

630 Yields 

631 ------ 

632 EventArray 

633 An array with the events 

634 

635 Examples 

636 -------- 

637 >>> reader = EventReader(b"% format EVT3;height=720;width=1280\\n% end\\n", n_events=5000) 

638 >>> for events in reader: 

639 ... print(f"Received chunk of {len(events)} events") 

640 Received chunk of 0 events 

641 

642 """ 

643 it: Any 

644 if self._async_read: 

645 self._check_no_active_prefetch() 

646 it = PrefetchIterator( 

647 self._iter_sync(), 

648 on_finish=lambda: setattr(self, "_active_prefetch", None), 

649 ) 

650 self._active_prefetch = it 

651 else: 

652 it = self._iter_sync() 

653 if self._real_time: 

654 # Pace on the consumer side, never inside the decode path: with 

655 # async_read the worker keeps decoding ahead while we sleep. 

656 return self._paced_iter(it) 

657 return it 

658 

659 def _paced_iter(self, it: Any) -> Any: 

660 """Wrap an iterator so each chunk is released on the playback clock.""" 

661 try: 

662 for chunk in it: 

663 self._pace(chunk) 

664 yield chunk 

665 finally: 

666 # Propagate early termination (break / close) to a prefetching 

667 # iterator so its worker thread is released. 

668 if hasattr(it, "close"): 

669 it.close() 

670 

671 def _iter_sync(self) -> Any: 

672 """The plain synchronous window generator behind :meth:`__iter__`.""" 

673 if not self._is_initialized: 

674 self.init() 

675 while not self.is_eof(): 

676 yield self._read() 

677 

678 def shape(self) -> tuple[int|None, int|None]: 

679 """Get the shape of the frame. 

680 

681 Returns 

682 ------- 

683 tuple[int, int] 

684 The shape of the frame (width, height) 

685 

686 """ 

687 if not self._is_initialized: 

688 self.init() 

689 if self._width is not None and self._height is not None: 

690 return self._width, self._height 

691 else: 

692 return self._file_decoder.shape() 

693 

694 

695 def tell(self) -> int: 

696 """Get the current position in the file. 

697 

698 Returns 

699 ------- 

700 int 

701 The current position in the file 

702 

703 """ 

704 if not self._is_initialized: 

705 self.init() 

706 return self._file_decoder.tell() 

707 

708 

709