Coverage for src/evutils/io/_event_writer.py: 78%

58 statements  

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

1"""Event writer module. 

2 

3Provides the `EventWriter` class for writing event data to various file formats. 

4""" 

5 

6import io 

7from datetime import datetime 

8from pathlib import Path 

9from typing import Any 

10 

11import numpy as np 

12 

13from . import encoders as ev_encoders 

14 

15 

16class EventWriter(): 

17 """Base class for writing events to different file formats. 

18 

19 Parameters 

20 ---------- 

21 file 

22 Path to the data file 

23 width 

24 Width of the frame, by default 1280 (not relevant for some formats) 

25 height 

26 Height of the frame, by default 720 (not relevant for some formats) 

27 dt 

28 Timestamp of the recording (default is the current time, but information is not saved in all formats) 

29 file_encoder 

30 File encoder to use, by default None (chosen from the file extension) 

31 **kwargs 

32 Additional arguments for the file encoder 

33 

34 

35 Examples 

36 -------- 

37 >>> import numpy as np 

38 >>> from evutils.types import EventArray 

39 >>> # Create an EventArray 

40 >>> events = EventArray( 

41 ... t=np.array([0, 1000]), 

42 ... x=np.array([0, 10]), 

43 ... y=np.array([0, 10]), 

44 ... p=np.array([1, 0]) 

45 ... ) 

46 >>> # Write the events to a raw file 

47 >>> with EventWriter("events.raw") as writer: # doctest: +SKIP 

48 ... writer.write(events) # doctest: +SKIP 

49 

50 """ 

51 

52 def __init__(self, file: Path | str | io.BufferedIOBase, width:int=1280, height:int=720, dt: datetime|None = None, file_encoder: ev_encoders.EventEncoder | None = None, **kwargs: Any): 

53 

54 self._file_name: Path | None = None 

55 

56 # Handle paths as input 

57 if isinstance(file, str): 

58 file = Path(file) 

59 if isinstance(file, Path): 

60 self._file_name = file 

61 file = self._open_file(file) 

62 else: 

63 # A raw stream was passed - we need an explicit encoder. 

64 if file_encoder is None: 

65 raise ValueError("When using a binary stream as file, the file_encoder must be provided explicitly") 

66 

67 if isinstance(file, io.IOBase) and not file.writable(): 

68 raise IOError("File is not writable") 

69 self._file: io.BufferedIOBase = file 

70 

71 # Resolve the encoder: explicit instance > heuristic from extension. 

72 if file_encoder is None: 

73 assert self._file_name is not None 

74 encoder_cls = ev_encoders.get_file_writer(self._file_name) 

75 self._file_encoder = encoder_cls(self._file, width=width, height=height, dt=dt, **kwargs) 

76 else: 

77 self._file_encoder = file_encoder 

78 

79 self._width = width 

80 self._height = height 

81 self._n_written_events = 0 

82 self._is_initialized = False 

83 self._dt = dt if dt is not None else datetime.now() 

84 

85 def _open_file(self, file_name: Path) -> io.BufferedIOBase: 

86 """Open the file for writing. 

87 

88 Parameters 

89 ---------- 

90 file_name : Path 

91 Path to the file to open. 

92 

93 Returns 

94 ------- 

95 io.BufferedIOBase 

96 The opened file object. 

97 

98 """ 

99 # 'w+b' (not 'wb'): container encoders (HDF5) need the stream to be 

100 # readable and seekable, and it costs nothing for the append-only ones. 

101 return open(str(file_name), 'w+b') 

102 

103 def init(self) -> None: 

104 """Initialize the writer (e.g. open the file, write the header). 

105 

106 This method can be called explicitly, but it is also called automatically when the first event is written 

107 """ 

108 self._file_encoder.init() 

109 self._is_initialized = True 

110 

111 def write(self, events: np.ndarray, triggers: np.ndarray | None = None) -> int: 

112 """Write a buffer of events to the file. 

113 

114 Parameters 

115 ---------- 

116 events 

117 Buffer of events to write (structured array or EventArray) 

118 triggers 

119 Buffer of triggers to write (structured array or TriggerArray). Optional. 

120 

121 TODO: Add support for writing triggers to the file (currently only events are written) 

122  

123 Returns 

124 ------- 

125 int 

126 Number of events written 

127 

128 Examples 

129 -------- 

130 >>> from evutils.types import EventArray 

131 >>> events = EventArray(t=[0, 100], x=[10, 11], y=[20, 21], p=[1, 0]) 

132 >>> writer = EventWriter("events.raw") # doctest: +SKIP 

133 >>> num_written = writer.write(events) # doctest: +SKIP 

134 >>> print(f"Wrote {num_written} events") # doctest: +SKIP 

135 >>> writer.close() # doctest: +SKIP 

136 

137 """ 

138 n_written = self._file_encoder.write(events) 

139 self._n_written_events += n_written 

140 return n_written 

141 

142 def write_external_triggers(self, triggers: np.ndarray) -> int: 

143 """Write external triggers to the file. 

144 

145 Parameters 

146 ---------- 

147 triggers : np.ndarray 

148 Buffer of triggers to write (structured array or TriggerArray). 

149 

150 Returns 

151 ------- 

152 int 

153 Number of triggers written. 

154 

155 Examples 

156 -------- 

157 >>> from evutils.types import TriggerArray 

158 >>> triggers = TriggerArray(t=[1000, 2000], p=[1, 0], id=[0, 0]) 

159 >>> writer = EventWriter("events.raw") # doctest: +SKIP 

160 >>> writer.write_external_triggers(triggers) # doctest: +SKIP 

161 >>> writer.close() # doctest: +SKIP 

162 

163 """ 

164 from ..types import EventArray 

165 self._file_encoder.write(EventArray.empty(), triggers=triggers) 

166 return len(triggers) 

167 

168 def flush(self) -> None: 

169 """Flush the buffer to the file.""" 

170 self._file_encoder.flush() 

171 

172 def __enter__(self) -> "EventWriter": 

173 return self 

174 

175 def __repr__(self) -> str: 

176 if self._is_initialized: 

177 is_initialized_txt = f"Written {self._n_written_events} events" 

178 else: 

179 is_initialized_txt = "not initialized" 

180 return f"{self.__class__.__name__}(file={self._file} - {is_initialized_txt}, {self._width}x{self._height})" 

181 

182 def __len__(self) -> int: 

183 return self._n_written_events 

184 

185 def close(self) -> None: 

186 """Close the writer and release the resources. 

187 

188 Finalizes the encoder first (container formats write their archive / 

189 index here), then closes the underlying file. 

190 """ 

191 self._file_encoder.close() 

192 self._file.close() 

193 

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

195 self.close()