fftekwfm 0.3.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.3
2
+ Name: fftekwfm
3
+ Version: 0.3.0.1
4
+ Summary: Read Tektronix WFM files with FastFrame support.
5
+ Author: Guillaume Le Goc
6
+ Author-email: Guillaume Le Goc <guillaume.le-goc@lncmi.cnrs.fr>
7
+ Requires-Dist: numpy>=2.2.0
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+
11
+ # FFTekWFM
12
+
13
+ Tektronix [Waveform file format](https://download.tek.com/manual/Waveform-File-Format-Manual-077022011.pdf) reader.
14
+
15
+ Features :
16
+ + Support WFM file format version 1, 2 and 3
17
+ + Support FastFrame
18
+ + Support memory mapping
19
+ + Support loading frames with or without pre- and post-charge data
20
+
21
+ Header reader structure inspired by [TekWFM2](https://github.com/vongostev/tekwfm2).
22
+
23
+ ## Notice
24
+ Note this package works for *my* use-case but is not properly tested. Consider using [TekWFM2](https://github.com/vongostev/tekwfm2) for a more established solution.
25
+
26
+ ## Installation
27
+ `pip install git+https://gitlab.in2p3.fr/himagnetos/tekwfm.git`
28
+
29
+ ## Usage
30
+
31
+ Read a file with memory mapping and plot some time series from fast frames, unscaled :
32
+ ```python
33
+ import matplotlib.pyplot as plt
34
+ from fftekwfm import TekWFM
35
+
36
+ filename = "/path/to/tekfile.wfm"
37
+ tek = TekWFM(filename).load_frames().get_time_frame()
38
+ plt.figure()
39
+ plt.plot(tek.time_frame, tek.frames[:, [0, 8000, 15000]])
40
+ plt.xlabel("time (s)")
41
+ plt.ylabel("signal (a.u.)")
42
+ ```
43
+
44
+ Load every frames in-memory and do some calculation :
45
+ ```python
46
+ import matplotlib.pyplot as plt
47
+ import numpy as np
48
+ from fftekwfm import TekWFM
49
+
50
+ filename = "/path/to/tekfile.wfm"
51
+ tek = TekWFM(filename).load_frames(mmap=False)
52
+ Sn = np.abs(np.fft.rfft(tek.frames, axis=0))
53
+ f = np.fft.rfftfreq(sig.shape[0], d=tek.tscale) # tscale is the time between two samples
54
+ plt.figure()
55
+ plt.plot(f, S_mag)
56
+ plt.xlabel("frequency (Hz)")
57
+ plt.ylabel("magnitude")
58
+ ```
@@ -0,0 +1,48 @@
1
+ # FFTekWFM
2
+
3
+ Tektronix [Waveform file format](https://download.tek.com/manual/Waveform-File-Format-Manual-077022011.pdf) reader.
4
+
5
+ Features :
6
+ + Support WFM file format version 1, 2 and 3
7
+ + Support FastFrame
8
+ + Support memory mapping
9
+ + Support loading frames with or without pre- and post-charge data
10
+
11
+ Header reader structure inspired by [TekWFM2](https://github.com/vongostev/tekwfm2).
12
+
13
+ ## Notice
14
+ Note this package works for *my* use-case but is not properly tested. Consider using [TekWFM2](https://github.com/vongostev/tekwfm2) for a more established solution.
15
+
16
+ ## Installation
17
+ `pip install git+https://gitlab.in2p3.fr/himagnetos/tekwfm.git`
18
+
19
+ ## Usage
20
+
21
+ Read a file with memory mapping and plot some time series from fast frames, unscaled :
22
+ ```python
23
+ import matplotlib.pyplot as plt
24
+ from fftekwfm import TekWFM
25
+
26
+ filename = "/path/to/tekfile.wfm"
27
+ tek = TekWFM(filename).load_frames().get_time_frame()
28
+ plt.figure()
29
+ plt.plot(tek.time_frame, tek.frames[:, [0, 8000, 15000]])
30
+ plt.xlabel("time (s)")
31
+ plt.ylabel("signal (a.u.)")
32
+ ```
33
+
34
+ Load every frames in-memory and do some calculation :
35
+ ```python
36
+ import matplotlib.pyplot as plt
37
+ import numpy as np
38
+ from fftekwfm import TekWFM
39
+
40
+ filename = "/path/to/tekfile.wfm"
41
+ tek = TekWFM(filename).load_frames(mmap=False)
42
+ Sn = np.abs(np.fft.rfft(tek.frames, axis=0))
43
+ f = np.fft.rfftfreq(sig.shape[0], d=tek.tscale) # tscale is the time between two samples
44
+ plt.figure()
45
+ plt.plot(f, S_mag)
46
+ plt.xlabel("frequency (Hz)")
47
+ plt.ylabel("magnitude")
48
+ ```
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "fftekwfm"
3
+ version = "0.3.0.1"
4
+ description = "Read Tektronix WFM files with FastFrame support."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Guillaume Le Goc", email = "guillaume.le-goc@lncmi.cnrs.fr" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "numpy>=2.2.0",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.9.9,<0.10.0"]
16
+ build-backend = "uv_build"
@@ -0,0 +1,3 @@
1
+ from .wfm import TekWFM
2
+
3
+ __all__ = ["TekWFM"]
File without changes
@@ -0,0 +1,29 @@
1
+ """
2
+ Initialize class to set its attributes types.
3
+ """
4
+
5
+
6
+ class tTekWFM:
7
+ byte_order: str
8
+ version: bytes
9
+ bps: int
10
+ curve_offset: int
11
+ nframes: int
12
+ fastframe: int
13
+ imp_dim_count: int
14
+ exp_dim_count: int
15
+ vscale: float
16
+ voffset: float
17
+ vunits: bytes
18
+ type_code: int
19
+ dformat: str
20
+ tscale: float
21
+ toffset: float
22
+ tunits: bytes
23
+ npoints: int
24
+ tsfrac: float
25
+ tsunix: int
26
+ pre_start_offset: int
27
+ data_start_offset: int
28
+ post_start_offset: int
29
+ post_stop_offset: int
@@ -0,0 +1,394 @@
1
+ """
2
+ TekWFM class to read Tektronix .wfm file.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from struct import unpack_from as unpk
7
+ from typing import Self
8
+
9
+ import numpy as np
10
+
11
+ from .ttypes import tTekWFM
12
+
13
+
14
+ class TekWFM(tTekWFM):
15
+ def __init__(self, filename: str | Path | None = None) -> None:
16
+ """
17
+ Read Tektronix WFM file format with metadata and FastFrames support.
18
+
19
+ Parameters
20
+ ----------
21
+ filename : str | Path | None
22
+ Full path to the .wfm file. Can be None to access methods.
23
+
24
+ """
25
+
26
+ if filename:
27
+ self.filename = filename # store file path
28
+
29
+ # Read metadata
30
+ header = self._load_header() # get header
31
+ meta = self.decode_header(header) # decode relevant information from header
32
+
33
+ # Store as attributes for convenience
34
+ for key, value in meta.items():
35
+ setattr(self, key, value)
36
+
37
+ # Get timestamps
38
+ self.get_timestamps()
39
+
40
+ def _load_header(self, n: int = 838) -> bytes:
41
+ """
42
+ Read bytes header.
43
+
44
+ Parameters
45
+ ----------
46
+ n : int, optional
47
+ Header size in bytes. Default is 838.
48
+
49
+ Returns
50
+ -------
51
+ header : bytes
52
+ Header ready to be decoded.
53
+
54
+ """
55
+ with open(self.filename, "rb") as f:
56
+ header = f.read(n)
57
+
58
+ return header
59
+
60
+ def _load_timestamps(self, n: int = 838) -> bytes:
61
+ """
62
+ Read bytes FastFrame timestamps.
63
+
64
+ Parameters
65
+ ----------
66
+ n : int, optional
67
+ Header size in bytes, by default 838.
68
+
69
+ Returns
70
+ -------
71
+ ts : bytes
72
+ Timestamps ready to be decoded.
73
+
74
+ """
75
+ with open(self.filename, "rb") as f:
76
+ f.seek(n) # skip header
77
+ bts = f.read((self.nframes - 1) * 24) # size of the WfmUpdateSpec objects
78
+
79
+ return bts
80
+
81
+ def decode_header(self, header: bytes) -> dict:
82
+ """
83
+ Decode WFM file header to obtain metadata.
84
+
85
+ Based on TekWFM2 [1] by Pavel Gostev (MIT Licence) and the Tektronix WFM file
86
+ format spec [2].
87
+
88
+ Parameters
89
+ ----------
90
+ header : bytes
91
+ Header bytes of the file.
92
+
93
+ Returns
94
+ -------
95
+ meta : dict
96
+ Metadata of the WFM file.
97
+
98
+ References
99
+ ----------
100
+ .. [1] https://github.com/vongostev/TekWFM2
101
+ .. [2] https://download.tek.com/manual/Waveform-File-Format-Manual-077022011.pdf
102
+
103
+ """
104
+ meta = {}
105
+
106
+ if len(header) != 838:
107
+ raise ValueError("WFM header bytes not 838")
108
+ byte_order = unpk("H", header, offset=0)[0]
109
+ if byte_order == 0x0F0F:
110
+ meta["byte_order"] = "<" # little-endian
111
+
112
+ elif meta["byte_order"] == 0xF0F0:
113
+ meta["byte_order"] = ">" # big-endian
114
+ else:
115
+ raise ValueError("Could not determine endianness.")
116
+
117
+ meta["version"] = unpk("8s", header, offset=2)[0]
118
+ if meta["version"] == b":WFM#001":
119
+ v1_offset = 2
120
+ elif meta["version"] == b":WFM#002":
121
+ v1_offset = 0
122
+ elif meta["version"] == b":WFM#003":
123
+ v1_offset = 0
124
+ else:
125
+ raise ValueError(
126
+ f"Only version 1, 2 and 3 of WFM supported, got {meta['version']}."
127
+ )
128
+
129
+ endianness = meta["byte_order"]
130
+ char_format = endianness + "c"
131
+ int_format = endianness + "i"
132
+ uint_format = endianness + "I"
133
+ byte_format = endianness + "b"
134
+ ulong_format = endianness + "L"
135
+ double_format = endianness + "d"
136
+
137
+ ## Waveform static file information
138
+ # Number of bytes per sample
139
+ meta["bps"] = unpk(byte_format, header, offset=15)[0]
140
+ # Byte offset to beginning of curve buffer
141
+ meta["curve_offset"] = unpk(int_format, header, offset=16)[0]
142
+ # Number of FastFrames
143
+ meta["nframes"] = unpk(uint_format, header, offset=72)[0] + 1
144
+
145
+ ## Waveform header
146
+ # 0 : Single waveform set, 1 : FastFrame set
147
+ meta["fastframe"] = unpk(uint_format, header, offset=78)[0]
148
+ # Number of implicit dimension (time)
149
+ meta["imp_dim_count"] = unpk(ulong_format, header, offset=114)[0]
150
+ # Number of explicit dimension (voltage)
151
+ meta["exp_dim_count"] = unpk(ulong_format, header, offset=118)[0]
152
+
153
+ ## Explicit dimension (vertical : voltage)
154
+ # Scaling to volts, Voltage = (wfmCurveData*vscale) + offset
155
+ meta["vscale"] = unpk(double_format, header, offset=168 - v1_offset)[0]
156
+ # offset
157
+ meta["voffset"] = unpk(double_format, header, offset=176 - v1_offset)[0]
158
+ # units
159
+ meta["vunits"] = unpk(char_format, header, offset=188 - v1_offset)[0]
160
+ # sample data type detection
161
+ meta["type_code"] = unpk(int_format, header, offset=240 - v1_offset)[0]
162
+ if meta["type_code"] == 0 and meta["bps"] == 2:
163
+ meta["dformat"] = endianness + "i2"
164
+ elif meta["type_code"] == 4 and meta["bps"] == 4:
165
+ meta["dformat"] = endianness + "f32"
166
+ else:
167
+ raise ValueError(
168
+ f"Data type code {meta['type_code']} or "
169
+ f"bytes-per-sample {meta['bps']} is not supported."
170
+ )
171
+
172
+ ## Implicit dimension (horizontal : time)
173
+ # scaling
174
+ meta["tscale"] = unpk(double_format, header, offset=488 - v1_offset)[0]
175
+ # offset
176
+ meta["toffset"] = unpk(double_format, header, offset=496 - v1_offset)[0]
177
+ # units
178
+ meta["tunits"] = unpk(char_format, header, 508 - v1_offset)[0]
179
+ # number of points per frame
180
+ meta["npoints"] = unpk(ulong_format, header, offset=504 - v1_offset)[0]
181
+
182
+ ## Wfm Update specification : trigger timestamp for first frame
183
+ meta["tsfrac"] = unpk(double_format, header, offset=796 - v1_offset)[0]
184
+ meta["tsunix"] = unpk(uint_format, header, offset=804 - v1_offset)[0]
185
+
186
+ ## Wfm Curve information
187
+ # Precharge start offset
188
+ meta["pre_start_offset"] = unpk(ulong_format, header, offset=818 - v1_offset)[0]
189
+ # Data start offset : bytes from beginning of curve buffer to first point
190
+ meta["data_start_offset"] = unpk(ulong_format, header, offset=822 - v1_offset)[
191
+ 0
192
+ ]
193
+ # Postcharge start offset
194
+ meta["post_start_offset"] = unpk(ulong_format, header, offset=826 - v1_offset)[
195
+ 0
196
+ ]
197
+ # Postcharge stop offset
198
+ meta["post_stop_offset"] = unpk(ulong_format, header, offset=830 - v1_offset)[0]
199
+
200
+ return meta
201
+
202
+ def _decode_fastframe_timestamps(
203
+ self, tsbytes: bytes, k: int, endianness: None | str = None
204
+ ) -> float:
205
+ """
206
+ Decode timestamp for the (k + 1)th frame.
207
+
208
+ Parameters
209
+ ----------
210
+ tsbytes : bytes
211
+ Binary data from WfmUpdateSpec object.
212
+ k : int
213
+ Index of the frame. 1-based because the first (0th) frame is stored
214
+ elsewhere in memory.
215
+ endianness : None or str, optionnal
216
+ Endianness. If None (default), read from self.
217
+
218
+ Returns
219
+ -------
220
+ ts : float
221
+ Absolute timestamp in second of the start trigger of the (k + 1)th frame.
222
+
223
+ """
224
+ if not endianness:
225
+ endianness = self.meta["byte_order"]
226
+
227
+ # fraction of second
228
+ tsfrac = unpk(f"{endianness}d", tsbytes, offset=12 + k * 24)[0]
229
+ # unix epoch
230
+ tsunix = unpk(f"{endianness}l", tsbytes, offset=20 + k * 24)[0]
231
+
232
+ return tsfrac + tsunix
233
+
234
+ def _load_frames(
235
+ self, pre: bool = False, post: bool = False, mmap: bool = True
236
+ ) -> np.ndarray | np.memmap:
237
+ """
238
+ Load all available frames. Optionaly, include pre- and post- charge data. By
239
+ default, memory-mapping is used instead of loading the whole array in RAM.
240
+
241
+ Parameters
242
+ ----------
243
+ pre, post : bool, optionnal
244
+ If True, include pre- and post-charge data. Default is False.
245
+ mmap : bool, optional
246
+ Use memory-mapping. Default is True.
247
+
248
+ Returns
249
+ -------
250
+ data : np.ndarray
251
+ Frames raw data with time series corresponding to one frame on columns.
252
+
253
+ """
254
+
255
+ # Number of samples : nframes * number of datapoints with pre/post charge
256
+ nsamples = self.nframes * self.post_stop_offset // self.bps
257
+
258
+ # Load data
259
+ if mmap:
260
+ data = np.memmap(
261
+ self.filename,
262
+ mode="r",
263
+ dtype=self.dformat,
264
+ offset=self.curve_offset,
265
+ shape=nsamples,
266
+ )
267
+ else:
268
+ data = np.fromfile(
269
+ self.filename,
270
+ dtype=self.dformat,
271
+ offset=self.curve_offset,
272
+ count=nsamples,
273
+ )
274
+
275
+ # Reshape with frames on columns
276
+ data = data.reshape(self.post_stop_offset // self.bps, self.nframes, order="F")
277
+
278
+ # Remove pre-charge data
279
+ if not pre:
280
+ start = self.data_start_offset // self.bps
281
+ else:
282
+ start = 0
283
+ # Remove post-charge data
284
+ if not post:
285
+ stop = start + self.npoints
286
+ else:
287
+ stop = None
288
+
289
+ return data[start:stop, :]
290
+
291
+ def _get_time_frame(self, start: float, dt: float, nsamples: int) -> np.ndarray:
292
+ """
293
+ Generate a time vector.
294
+
295
+ Parameters
296
+ ----------
297
+ start : float
298
+ Value of first sample.
299
+ dt : float
300
+ Time between to samples.
301
+ nsamples : int
302
+ Number of time points.
303
+
304
+ Returns
305
+ -------
306
+ time_frame : np.ndarray
307
+ Time vector.
308
+
309
+ """
310
+
311
+ tstop = start + nsamples * dt
312
+ return np.linspace(start, tstop, nsamples, endpoint=False)
313
+
314
+ def scale_data(self, data: np.ndarray) -> np.ndarray:
315
+ """
316
+ Convert input data to physical units (usually, volts)) given conversion factor
317
+ in metadata.
318
+
319
+ Note that if converting all the data at once, if it was in INT16, it will be
320
+ broadcasted to float and will be very slow. It is advised to perform all
321
+ necessary computations and reduction before conversion.
322
+
323
+ Parameters
324
+ ----------
325
+ data : np.ndarray
326
+ Input data to convert.
327
+
328
+ Returns
329
+ -------
330
+ data_scale : np.ndarray
331
+ Data scaled to physical units.
332
+
333
+ """
334
+ return data * self.vscale + self.voffset
335
+
336
+ def get_timestamps(self) -> Self:
337
+ """
338
+ Load, decode and store FastFrames onsets as an numpy array.
339
+
340
+ """
341
+
342
+ # Get timestamps bytes
343
+ bts = self._load_timestamps()
344
+
345
+ # Preallocate array
346
+ frame_onsets = np.empty(self.nframes, dtype=float)
347
+
348
+ # Get first frame timestamps
349
+ frame_onsets[0] = self.tsunix + self.tsfrac
350
+
351
+ # Get the rest
352
+ frame_onsets[1:] = [
353
+ self._decode_fastframe_timestamps(bts, k, endianness=self.byte_order)
354
+ for k in range(self.nframes - 1)
355
+ ]
356
+
357
+ # Start at 0
358
+ frame_onsets -= frame_onsets[0]
359
+
360
+ # Store
361
+ self.frame_onsets = frame_onsets
362
+
363
+ return self
364
+
365
+ def load_frames(self, **kwargs) -> Self:
366
+ """
367
+ Wrap the `_load_frames()` method and stores the loaded array as an attribute.
368
+
369
+ """
370
+ self.frames = self._load_frames(**kwargs)
371
+
372
+ return self
373
+
374
+ def get_time_frame(self) -> Self:
375
+ """
376
+ Generate a time vector corresponding to one frame. Require the frames to be
377
+ loaded so we know how many points there are (eg. if pre- and post-charge data
378
+ was included), otherwise assume pre- and post-charge data is not included.
379
+
380
+ """
381
+ start = self.toffset
382
+ dt = self.tscale
383
+ if hasattr(self, "frames"):
384
+ nsamples = self.frames.shape[0]
385
+ else:
386
+ print(
387
+ "[Warn] Frames not loaded, assuming time vector without pre- and "
388
+ "post-charge."
389
+ )
390
+ nsamples = self.npoints
391
+
392
+ self.time_frame = self._get_time_frame(start, dt, nsamples)
393
+
394
+ return self