dbdreader 0.6.0.dev1__tar.gz → 0.6.2__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.
Files changed (90) hide show
  1. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/PKG-INFO +27 -4
  2. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/README.md +24 -0
  3. dbdreader-0.6.2/dbdreader/_dbdreader.py +312 -0
  4. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/dbdreader.py +1086 -771
  5. dbdreader-0.6.2/dbdreader/decompress.py +339 -0
  6. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/PKG-INFO +27 -4
  7. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/SOURCES.txt +1 -0
  8. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/requires.txt +0 -1
  9. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/decompress.c +13 -5
  10. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/pyproject.toml +8 -7
  11. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/requirements.txt +0 -1
  12. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/tests/test_dbdreader.py +0 -7
  13. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/tests/test_decompress.py +35 -20
  14. dbdreader-0.6.0.dev1/dbdreader/decompress.py +0 -247
  15. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/COPYING +0 -0
  16. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/COPYRIGHT +0 -0
  17. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/INSTALL.rst +0 -0
  18. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/LICENSE +0 -0
  19. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/MANIFEST.in +0 -0
  20. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/__init__.py +0 -0
  21. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.dcd +0 -0
  22. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.ebd +0 -0
  23. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.ecd +0 -0
  24. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.mcg +0 -0
  25. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.mlg +0 -0
  26. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600001.dbd +0 -0
  27. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600001.dcd +0 -0
  28. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600001.ebd +0 -0
  29. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600001.ecd +0 -0
  30. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/02380107.ecd +0 -0
  31. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/02380108.ecd +0 -0
  32. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/02450133.tcd +0 -0
  33. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/02450137.tcd +0 -0
  34. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-203-00-000.SBD +0 -0
  35. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-203-00-000.TBD +0 -0
  36. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-000.dbd +0 -0
  37. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-000.ebd +0 -0
  38. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-000.sbd +0 -0
  39. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-000.tbd +0 -0
  40. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-001.sbd +0 -0
  41. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-001.tbd +0 -0
  42. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-002.sbd +0 -0
  43. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-002.tbd +0 -0
  44. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/ammonite-2008-028-01-000.mbd +0 -0
  45. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/06a36d4e.cac +0 -0
  46. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/06a36d4e.ccc +0 -0
  47. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/093bd5ed.cac +0 -0
  48. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/0f682cb2.cac +0 -0
  49. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/39b2c12e.cac +0 -0
  50. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/468fd1be.cac +0 -0
  51. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/5fbf5947.cac +0 -0
  52. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/61b1780f.cac +0 -0
  53. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/6f9a08d3.cac +0 -0
  54. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/813b137d.cac +0 -0
  55. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/8d31d0c8.cac +0 -0
  56. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/92610b65.cac +0 -0
  57. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/c4ec741e.cac +0 -0
  58. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/d6f44165.cac +0 -0
  59. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/daad1b20.cac +0 -0
  60. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/daad1b20.ccc +0 -0
  61. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/dc76ebd5.cac +0 -0
  62. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac_missing/d6f44165.cac +0 -0
  63. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/dbd2asc_output.txt +0 -0
  64. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/electa-2023-143-00-050.sbd +0 -0
  65. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/electa-2023-143-00-050.tbd +0 -0
  66. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/empty-2014-204-05-000.dbd +0 -0
  67. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/hal_1002-2024-183-4-4.sbd +0 -0
  68. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/hal_1002-2024-183-4-4.tbd +0 -0
  69. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/hal_1002-2024-183-4-6.tbd +0 -0
  70. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/invalid_encoding-2014-204-05-000.dbd +0 -0
  71. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/sebastian-2014-204-05-000.dbd +0 -0
  72. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/sebastian-2014-204-05-000.ebd +0 -0
  73. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/sebastian-2014-204-05-001.dbd +0 -0
  74. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/sebastian-2014-204-05-001.ebd +0 -0
  75. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/unit_887-2021-321-3-0.sbd +0 -0
  76. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/unit_887-2021-321-3-0.tbd +0 -0
  77. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/scripts.py +0 -0
  78. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/dependency_links.txt +0 -0
  79. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/entry_points.txt +0 -0
  80. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/top_level.txt +0 -0
  81. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/examples/read_multiple_dbd_files.py +0 -0
  82. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/examples/read_single_dbd_file.py +0 -0
  83. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/dbdreader.c +0 -0
  84. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/include/dbdreader.h +0 -0
  85. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/include/decompress.h +0 -0
  86. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/py_dbdreader.c +0 -0
  87. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/lz4/include/lz4.h +0 -0
  88. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/lz4/lz4.c +0 -0
  89. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/setup.cfg +0 -0
  90. {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/setup.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbdreader
3
- Version: 0.6.0.dev1
3
+ Version: 0.6.2
4
4
  Summary: A python module to access binary data files generated by Teledyne WebbResearch gliders
5
5
  Author-email: Lucas Merckelbach <lucas.merckelbach@hereon.de>
6
- License: GPL-3.0
6
+ License: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://dbdreader.readthedocs.io/en/latest/
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Intended Audience :: Science/Research
10
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
11
  Classifier: Operating System :: POSIX
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
@@ -15,7 +15,6 @@ License-File: LICENSE
15
15
  License-File: COPYING
16
16
  Requires-Dist: lz4>=4.4.5
17
17
  Requires-Dist: numpy>=2.2.6
18
- Requires-Dist: scipy>=1.15.3
19
18
  Dynamic: license-file
20
19
 
21
20
  [![PyPI version](https://badgen.net/pypi/v/dbdreader)](https://pypi.org/project/dbdreader)
@@ -34,6 +33,30 @@ binary files, so that they can be further analysed.
34
33
 
35
34
  ## Change log
36
35
 
36
+ ### Version 0.6.2
37
+
38
+ * Builds wheels for python versions 3.10 -- 3.14, including macos
39
+ * Removes the get_list() method from the DBD class
40
+ * Adds CI tests for all platforms (contributed by jklymak).
41
+ * Typehinting introduced
42
+
43
+ ### Version 0.6.1
44
+
45
+ * Drops dependency on scipy.
46
+
47
+ ### Version 0.6.0
48
+
49
+ * Offers a python-only implementation, alongside the C-extension to
50
+ read the binary files. At the expense of processing speed, no
51
+ compiler is required to use this package. Python-only code
52
+ contributed by jklymak.
53
+
54
+ * A github workflow is implemented that generates pre-compiled wheels
55
+ for linux and windows platforms for python version 3.10, 3.11, 3.12
56
+ and 3.13.
57
+
58
+ * Some speed improvements of the C-extenstion
59
+
37
60
  ### version 0.5.9
38
61
 
39
62
  * Handles reading errors that can occur when a compressed file turns
@@ -14,6 +14,30 @@ binary files, so that they can be further analysed.
14
14
 
15
15
  ## Change log
16
16
 
17
+ ### Version 0.6.2
18
+
19
+ * Builds wheels for python versions 3.10 -- 3.14, including macos
20
+ * Removes the get_list() method from the DBD class
21
+ * Adds CI tests for all platforms (contributed by jklymak).
22
+ * Typehinting introduced
23
+
24
+ ### Version 0.6.1
25
+
26
+ * Drops dependency on scipy.
27
+
28
+ ### Version 0.6.0
29
+
30
+ * Offers a python-only implementation, alongside the C-extension to
31
+ read the binary files. At the expense of processing speed, no
32
+ compiler is required to use this package. Python-only code
33
+ contributed by jklymak.
34
+
35
+ * A github workflow is implemented that generates pre-compiled wheels
36
+ for linux and windows platforms for python version 3.10, 3.11, 3.12
37
+ and 3.13.
38
+
39
+ * Some speed improvements of the C-extenstion
40
+
17
41
  ### version 0.5.9
18
42
 
19
43
  * Handles reading errors that can occur when a compressed file turns
@@ -0,0 +1,312 @@
1
+ """
2
+ Pure Python/NumPy replacement for the _dbdreader C extension.
3
+
4
+ Reads Slocum glider binary DBD/SBD/MBD/etc. files. The public API is a
5
+ single function ``get()`` whose signature matches the C extension exactly.
6
+
7
+ Binary format recap
8
+ -------------------
9
+ After the ASCII header there is a "known cycle" (17 bytes) that the glider
10
+ always writes with fixed values so the reader can detect byte order:
11
+
12
+ [0] 's' cycle-tag byte
13
+ [1] <1-byte int>
14
+ [2:4] 0x1234 two-byte uint (little-endian if == 4660 decimal)
15
+ [4:8] 123.456 four-byte float
16
+ [8:16] 123456789... eight-byte double
17
+ [16] 'd' separator before first real cycle
18
+
19
+ Every subsequent cycle looks like:
20
+
21
+ <n_state_bytes> 2-bit-packed state fields (4 fields/byte, MSB first)
22
+ each field: UPDATED=2, SAME=1, NOTSET=0
23
+ <data bytes> concatenated raw values for UPDATED sensors only,
24
+ in ascending sensor-index order
25
+ 'd' (0x64) separator before the next cycle's state bytes
26
+
27
+ Performance strategy
28
+ --------------------
29
+ Two-pass approach to minimise Python overhead:
30
+
31
+ Pass 1 – cheap Python loop over cycles to locate cycle boundaries.
32
+ Uses a pre-built per-byte-position lookup table so each cycle
33
+ costs only n_state_bytes Python ops (not n_sensors).
34
+
35
+ Pass 2 – batch numpy operations over ALL cycles at once:
36
+ • extract state bytes for every cycle in one fancy-index read
37
+ • decode fields and compute chunk offsets with vectorised ops
38
+ • read sensor values for wanted sensors with vectorised byte extraction
39
+
40
+ This replaces the original approach of ~8 numpy array allocations per cycle
41
+ inside a Python loop, which dominated runtime for files with many cycles.
42
+ """
43
+
44
+ import struct
45
+ from typing import Any
46
+
47
+ import numpy as np
48
+
49
+ # ── constants (mirror dbdreader.h) ───────────────────────────────────────────
50
+ FILLVALUE = 1e9
51
+ UPDATED = 2
52
+ SAME = 1
53
+ NOTSET = 0
54
+ _FMT_CHAR = {2: "h", 4: "f", 8: "d"} # byte-size → struct char
55
+
56
+ # Shift amounts to extract 4 × 2-bit fields from one byte (MSB first)
57
+ _SHIFTS = np.array([6, 4, 2, 0], dtype=np.int32)
58
+
59
+
60
+ # ── helpers ──────────────────────────────────────────────────────────────────
61
+
62
+
63
+ def _read_file(filename: str) -> bytes:
64
+ """Return raw decompressed bytes for *filename* (handles .?cd/.?cg files)."""
65
+ from dbdreader.decompress import is_compressed, Decompressor
66
+
67
+ if is_compressed(filename):
68
+ with Decompressor(filename) as d:
69
+ return d.decompress()
70
+ with open(filename, "rb") as fh:
71
+ return fh.read()
72
+
73
+
74
+ def _insert_ti(vi_list: list[int], ti: int) -> tuple[list[int], int]:
75
+ """
76
+ Insert *ti* into sorted *vi_list* before the first element > ti,
77
+ replicating the C code in get_variable().
78
+
79
+ Returns (vit, nti) where vit is the extended list and nti is the index
80
+ of ti within vit.
81
+ """
82
+ vit = []
83
+ i = 0
84
+ while i < len(vi_list):
85
+ if vi_list[i] > ti:
86
+ break
87
+ vit.append(vi_list[i])
88
+ i += 1
89
+ vit.append(ti)
90
+ nti = i
91
+ for j in range(i, len(vi_list)):
92
+ vit.append(vi_list[j])
93
+ return vit, nti
94
+
95
+
96
+ def _build_chunk_lut(
97
+ n_state_bytes: int, n_sensors: int, bs_list: list[int]
98
+ ) -> list[list[int]]:
99
+ """
100
+ Build a lookup table: lut[byte_pos][byte_value] = total data bytes
101
+ contributed to the chunk by the four sensors encoded in that state byte.
102
+
103
+ Used in Pass 1 to compute chunksize per cycle in O(n_state_bytes) ops.
104
+ """
105
+ byte_vals = np.arange(256, dtype=np.int32)
106
+ lut = np.zeros((n_state_bytes, 256), dtype=np.int32)
107
+ for bpos in range(n_state_bytes):
108
+ for slot in range(4):
109
+ sidx = bpos * 4 + slot
110
+ if sidx >= n_sensors:
111
+ break
112
+ shift = 6 - slot * 2 # 6, 4, 2, 0
113
+ states = (byte_vals >> shift) & 3 # (256,)
114
+ lut[bpos] += (states == UPDATED) * bs_list[sidx]
115
+ return lut.tolist() # nested Python list for fast indexing
116
+
117
+
118
+ # ── public API ───────────────────────────────────────────────────────────────
119
+
120
+
121
+ def get(
122
+ n_state_bytes: int,
123
+ n_sensors: int,
124
+ bin_offset: int,
125
+ byte_sizes: tuple[int, ...],
126
+ filename: str,
127
+ ti: int,
128
+ vi: tuple[int, ...],
129
+ return_nans: int,
130
+ skip_initial_line: int,
131
+ max_values_to_read: int,
132
+ ) -> tuple[int, list[Any]]:
133
+ """
134
+ Read one or more sensor time-series from a glider binary data file.
135
+
136
+ Parameters match the C extension exactly:
137
+
138
+ n_state_bytes : int – number of state bytes per cycle
139
+ n_sensors : int – total sensors in the file
140
+ bin_offset : int – byte offset to the start of binary data
141
+ byte_sizes : tuple – byte size for each of the n_sensors sensors
142
+ filename : str – path to the data file (compressed or not)
143
+ ti : int – sensor index of the time variable
144
+ vi : tuple – sorted sensor indices to retrieve
145
+ return_nans : int – 1 → include NOTSET slots as FILLVALUE rows
146
+ skip_initial_line : int – 1 → discard first data cycle
147
+ max_values_to_read : int – stop after this many rows (0 = unlimited)
148
+
149
+ Returns
150
+ -------
151
+ (error_no, result) where error_no is 0 on success and result is a
152
+ list of 2*nv lists:
153
+ [t_0, t_1, …, t_{nv-1}, v_0, v_1, …, v_{nv-1}]
154
+ where each t_i / v_i is a Python list of floats.
155
+ """
156
+
157
+ try:
158
+ data = _read_file(filename)
159
+ except FileNotFoundError:
160
+ return 2, [] # ERROR_FILE_NOT_FOUND
161
+ except Exception:
162
+ return 1, [] # ERROR_UNEXPECTED_END_OF_FILE
163
+ nv = len(vi)
164
+ bs_list = list(byte_sizes)
165
+ vit, nti = _insert_ti(list(vi), ti)
166
+ nvt = len(vit) # = nv + 1
167
+
168
+ # ── byte-order detection ─────────────────────────────────────────────────
169
+ endian = "<" if struct.unpack_from("<H", data, bin_offset + 2)[0] == 4660 else ">"
170
+
171
+ # ── numpy array of byte sizes (used in vectorised pass 2) ────────────────
172
+ bs_arr = np.array(bs_list, dtype=np.int32) # (n_sensors,)
173
+ vit_arr = np.array(vit, dtype=np.intp) # (nvt,)
174
+
175
+ # ── chunk-size lookup table (for pass 1) ─────────────────────────────────
176
+ chunk_lut = _build_chunk_lut(n_state_bytes, n_sensors, bs_list)
177
+
178
+ # ── PASS 1: locate cycle boundaries ──────────────────────────────────────
179
+ # For each cycle record (state_bytes + data_chunk + separator):
180
+ # * record the file position of the state bytes
181
+ # * compute the data-chunk size using the lookup table
182
+ # Cost: O(n_cycles × n_state_bytes) pure-Python ops — cheap.
183
+
184
+ min_offset_value = -2 if return_nans else -1
185
+ file_size = len(data)
186
+ pos = bin_offset + 17 # skip known cycle
187
+ write_data = not bool(skip_initial_line)
188
+
189
+ state_positions = [] # file pos of state bytes for each cycle
190
+ chunk_sizes = [] # data-chunk byte count for each cycle
191
+ write_flags = [] # whether to emit output for each cycle
192
+
193
+ while pos < file_size:
194
+ if pos + n_state_bytes > file_size:
195
+ break
196
+
197
+ # Chunksize via LUT: O(n_state_bytes) lookups
198
+ chunksize = 0
199
+ for bpos in range(n_state_bytes):
200
+ chunksize += chunk_lut[bpos][data[pos + bpos]]
201
+
202
+ state_positions.append(pos)
203
+ chunk_sizes.append(chunksize)
204
+ write_flags.append(write_data)
205
+
206
+ pos += n_state_bytes + chunksize + 1
207
+ write_data = True
208
+
209
+ n_cycles = len(state_positions)
210
+ if n_cycles == 0:
211
+ return 0, [[] for _ in range(nv)] + [[] for _ in range(nv)]
212
+
213
+ # ── PASS 2: vectorised processing of all cycles at once ──────────────────
214
+ data_u8 = np.frombuffer(data, dtype=np.uint8)
215
+ sp_arr = np.array(state_positions, dtype=np.intp) # (n_cycles,)
216
+ cstart = sp_arr + n_state_bytes # chunk start positions
217
+
218
+ # Extract state bytes for ALL cycles: shape (n_cycles, n_state_bytes)
219
+ sb_idx = sp_arr[:, np.newaxis] + np.arange(n_state_bytes, dtype=np.intp)
220
+ all_sb = data_u8[sb_idx] # uint8, (n_cycles, n_state_bytes)
221
+
222
+ # Decode 2-bit fields: (n_cycles, n_state_bytes, 4) → (n_cycles, n_sensors)
223
+ all_fields = (
224
+ all_sb[:, :, np.newaxis] >> _SHIFTS
225
+ ) & 3 # int, (n_cycles, n_state_bytes, 4)
226
+ all_fields = all_fields.reshape(n_cycles, n_state_bytes * 4)[:, :n_sensors]
227
+
228
+ # Compute byte offsets within the data chunk for every sensor in every cycle.
229
+ # UPDATED → cumulative offset; SAME → -1; NOTSET → -2.
230
+ upd_mask = all_fields == UPDATED # bool, (n_cycles, n_sensors)
231
+ w_bs = np.where(upd_mask, bs_arr[np.newaxis, :], 0) # int32, (n_cycles, n_sensors)
232
+ cum_bs = np.cumsum(w_bs, axis=1) # int32, (n_cycles, n_sensors)
233
+ all_off = np.where(upd_mask, cum_bs - w_bs, np.int32(-1)) # (n_cycles, n_sensors)
234
+ all_off = np.where(all_fields == NOTSET, np.int32(-2), all_off)
235
+
236
+ # Offsets for only the wanted sensors: (n_cycles, nvt)
237
+ wanted_off = all_off[:, vit_arr] # int32, (n_cycles, nvt)
238
+
239
+ # ── helper: read all values for one column of wanted_off ─────────────────
240
+ def _read_col(col: int) -> np.ndarray[Any, Any]:
241
+ """
242
+ Return float64 array (n_cycles,) for wanted sensor at column *col*.
243
+ UPDATED cycles → actual data value.
244
+ SAME cycles → carry-forward of last UPDATED value (numpy ffill).
245
+ NOTSET cycles → NaN (or FILLVALUE if return_nans).
246
+ """
247
+ sidx = vit[col]
248
+ bs = bs_list[sidx]
249
+ offsets = wanted_off[:, col] # int32, (n_cycles,)
250
+ vals = np.full(n_cycles, np.nan, dtype=np.float64)
251
+
252
+ upd = np.where(offsets >= 0)[0] # indices of UPDATED cycles
253
+ if upd.size:
254
+ abs_pos = cstart[upd] + offsets[upd].astype(np.intp)
255
+
256
+ if bs == 1:
257
+ raw = data_u8[abs_pos].view(np.int8).astype(np.float64)
258
+ vals[upd] = raw
259
+ else:
260
+ # Gather bs consecutive bytes for each updated cycle,
261
+ # reinterpret as the sensor's native type.
262
+ bidx = abs_pos[:, np.newaxis] + np.arange(bs, dtype=np.intp)
263
+ raw = data_u8[bidx].tobytes() # flat byte buffer
264
+ dt = np.dtype(endian + _FMT_CHAR[bs])
265
+ vals[upd] = np.frombuffer(raw, dtype=dt)
266
+
267
+ # Forward-fill: propagate each UPDATED value into subsequent SAME slots.
268
+ # Classic numpy trick: build an index array that "sticks" at the last
269
+ # non-NaN position, then use it to index vals.
270
+ has_val = ~np.isnan(vals)
271
+ ff_idx = np.where(has_val, np.arange(n_cycles, dtype=np.intp), np.intp(0))
272
+ np.maximum.accumulate(ff_idx, out=ff_idx)
273
+ vals = vals[ff_idx]
274
+
275
+ # NOTSET cycles: mark as FILLVALUE (if return_nans) or leave as
276
+ # carried-forward value (which will be excluded by the mask below).
277
+ if return_nans:
278
+ vals[offsets == np.int32(-2)] = FILLVALUE
279
+
280
+ return vals
281
+
282
+ # ── read time and sensor values ───────────────────────────────────────────
283
+ t_vals = _read_col(nti) # (n_cycles,) float64
284
+ write_arr = np.array(write_flags, dtype=bool)
285
+
286
+ result_t: list[Any] = [None] * nv
287
+ result_v: list[Any] = [None] * nv
288
+
289
+ for col in range(nvt):
290
+ if col == nti:
291
+ continue
292
+ j = col - (1 if col > nti else 0) # output index in result_t/v
293
+
294
+ v_vals = _read_col(col)
295
+ v_off = wanted_off[:, col]
296
+
297
+ # Include this cycle in output if:
298
+ # • write_flags says so (skip_initial_line handling)
299
+ # • sensor has a valid offset (UPDATED or SAME; also NOTSET if return_nans)
300
+ include = v_off >= min_offset_value
301
+ mask = write_arr & include
302
+
303
+ if max_values_to_read > 0:
304
+ keep = np.where(mask)[0]
305
+ if keep.size > max_values_to_read:
306
+ mask = np.zeros(n_cycles, dtype=bool)
307
+ mask[keep[:max_values_to_read]] = True
308
+
309
+ result_t[j] = t_vals[mask].tolist()
310
+ result_v[j] = v_vals[mask].tolist()
311
+
312
+ return 0, [result_t[j] for j in range(nv)] + [result_v[j] for j in range(nv)]