dbdreader 0.6.0__cp313-cp313-win_amd64.whl

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 (51) hide show
  1. _dbdreader.cp313-win_amd64.pyd +0 -0
  2. dbdreader/__init__.py +12 -0
  3. dbdreader/_dbdreader.py +294 -0
  4. dbdreader/data/01600000.dcd +0 -0
  5. dbdreader/data/01600000.ebd +0 -0
  6. dbdreader/data/01600000.ecd +0 -0
  7. dbdreader/data/01600000.mcg +0 -0
  8. dbdreader/data/01600000.mlg +1904 -0
  9. dbdreader/data/01600001.dbd +0 -0
  10. dbdreader/data/01600001.dcd +0 -0
  11. dbdreader/data/01600001.ebd +0 -0
  12. dbdreader/data/01600001.ecd +0 -0
  13. dbdreader/data/02380107.ecd +0 -0
  14. dbdreader/data/02380108.ecd +0 -0
  15. dbdreader/data/02450133.tcd +0 -0
  16. dbdreader/data/02450137.tcd +0 -0
  17. dbdreader/data/amadeus-2014-203-00-000.SBD +0 -0
  18. dbdreader/data/amadeus-2014-203-00-000.TBD +0 -0
  19. dbdreader/data/amadeus-2014-204-05-000.dbd +0 -0
  20. dbdreader/data/amadeus-2014-204-05-000.ebd +0 -0
  21. dbdreader/data/amadeus-2014-204-05-000.sbd +0 -0
  22. dbdreader/data/amadeus-2014-204-05-000.tbd +0 -0
  23. dbdreader/data/amadeus-2014-204-05-001.sbd +0 -0
  24. dbdreader/data/amadeus-2014-204-05-001.tbd +0 -0
  25. dbdreader/data/amadeus-2014-204-05-002.sbd +0 -0
  26. dbdreader/data/amadeus-2014-204-05-002.tbd +0 -0
  27. dbdreader/data/ammonite-2008-028-01-000.mbd +2091 -1
  28. dbdreader/data/dbd2asc_output.txt +25 -0
  29. dbdreader/data/electa-2023-143-00-050.sbd +0 -0
  30. dbdreader/data/electa-2023-143-00-050.tbd +0 -0
  31. dbdreader/data/empty-2014-204-05-000.dbd +0 -0
  32. dbdreader/data/hal_1002-2024-183-4-4.sbd +0 -0
  33. dbdreader/data/hal_1002-2024-183-4-4.tbd +0 -0
  34. dbdreader/data/hal_1002-2024-183-4-6.tbd +0 -0
  35. dbdreader/data/invalid_encoding-2014-204-05-000.dbd +0 -0
  36. dbdreader/data/sebastian-2014-204-05-000.dbd +0 -0
  37. dbdreader/data/sebastian-2014-204-05-000.ebd +0 -0
  38. dbdreader/data/sebastian-2014-204-05-001.dbd +0 -0
  39. dbdreader/data/sebastian-2014-204-05-001.ebd +0 -0
  40. dbdreader/data/unit_887-2021-321-3-0.sbd +0 -0
  41. dbdreader/data/unit_887-2021-321-3-0.tbd +0 -0
  42. dbdreader/dbdreader.py +2208 -0
  43. dbdreader/decompress.py +318 -0
  44. dbdreader/scripts.py +279 -0
  45. dbdreader-0.6.0.dist-info/METADATA +284 -0
  46. dbdreader-0.6.0.dist-info/RECORD +51 -0
  47. dbdreader-0.6.0.dist-info/WHEEL +5 -0
  48. dbdreader-0.6.0.dist-info/entry_points.txt +3 -0
  49. dbdreader-0.6.0.dist-info/licenses/COPYING +340 -0
  50. dbdreader-0.6.0.dist-info/licenses/LICENSE +674 -0
  51. dbdreader-0.6.0.dist-info/top_level.txt +2 -0
Binary file
dbdreader/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ import os
2
+ from .dbdreader import *
3
+
4
+ from importlib.metadata import version, PackageNotFoundError
5
+ try:
6
+ __version__ = version("dbdreader")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
9
+ __all__ = ['dbdreader']
10
+
11
+ EXAMPLE_DATA_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
12
+ 'data'))
@@ -0,0 +1,294 @@
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
+ import numpy as np
46
+
47
+
48
+ # ── constants (mirror dbdreader.h) ───────────────────────────────────────────
49
+ FILLVALUE = 1e9
50
+ UPDATED = 2
51
+ SAME = 1
52
+ NOTSET = 0
53
+ _FMT_CHAR = {2: 'h', 4: 'f', 8: 'd'} # byte-size → struct char
54
+
55
+ # Shift amounts to extract 4 × 2-bit fields from one byte (MSB first)
56
+ _SHIFTS = np.array([6, 4, 2, 0], dtype=np.int32)
57
+
58
+
59
+ # ── helpers ──────────────────────────────────────────────────────────────────
60
+
61
+ def _read_file(filename: str) -> bytes:
62
+ """Return raw decompressed bytes for *filename* (handles .?cd/.?cg files)."""
63
+ from dbdreader.decompress import is_compressed, Decompressor
64
+ if is_compressed(filename):
65
+ with Decompressor(filename) as d:
66
+ return d.decompress()
67
+ with open(filename, 'rb') as fh:
68
+ return fh.read()
69
+
70
+
71
+ def _insert_ti(vi_list: list, ti: int):
72
+ """
73
+ Insert *ti* into sorted *vi_list* before the first element > ti,
74
+ replicating the C code in get_variable().
75
+
76
+ Returns (vit, nti) where vit is the extended list and nti is the index
77
+ of ti within vit.
78
+ """
79
+ vit = []
80
+ i = 0
81
+ while i < len(vi_list):
82
+ if vi_list[i] > ti:
83
+ break
84
+ vit.append(vi_list[i])
85
+ i += 1
86
+ vit.append(ti)
87
+ nti = i
88
+ for j in range(i, len(vi_list)):
89
+ vit.append(vi_list[j])
90
+ return vit, nti
91
+
92
+
93
+ def _build_chunk_lut(n_state_bytes: int, n_sensors: int, bs_list: list) -> list:
94
+ """
95
+ Build a lookup table: lut[byte_pos][byte_value] = total data bytes
96
+ contributed to the chunk by the four sensors encoded in that state byte.
97
+
98
+ Used in Pass 1 to compute chunksize per cycle in O(n_state_bytes) ops.
99
+ """
100
+ byte_vals = np.arange(256, dtype=np.int32)
101
+ lut = np.zeros((n_state_bytes, 256), dtype=np.int32)
102
+ for bpos in range(n_state_bytes):
103
+ for slot in range(4):
104
+ sidx = bpos * 4 + slot
105
+ if sidx >= n_sensors:
106
+ break
107
+ shift = 6 - slot * 2 # 6, 4, 2, 0
108
+ states = (byte_vals >> shift) & 3 # (256,)
109
+ lut[bpos] += (states == UPDATED) * bs_list[sidx]
110
+ return lut.tolist() # nested Python list for fast indexing
111
+
112
+
113
+ # ── public API ───────────────────────────────────────────────────────────────
114
+
115
+ def get(n_state_bytes, n_sensors, bin_offset, byte_sizes,
116
+ filename, ti, vi, return_nans, skip_initial_line, max_values_to_read):
117
+ """
118
+ Read one or more sensor time-series from a glider binary data file.
119
+
120
+ Parameters match the C extension exactly:
121
+
122
+ n_state_bytes : int – number of state bytes per cycle
123
+ n_sensors : int – total sensors in the file
124
+ bin_offset : int – byte offset to the start of binary data
125
+ byte_sizes : tuple – byte size for each of the n_sensors sensors
126
+ filename : str – path to the data file (compressed or not)
127
+ ti : int – sensor index of the time variable
128
+ vi : tuple – sorted sensor indices to retrieve
129
+ return_nans : int – 1 → include NOTSET slots as FILLVALUE rows
130
+ skip_initial_line : int – 1 → discard first data cycle
131
+ max_values_to_read : int – stop after this many rows (0 = unlimited)
132
+
133
+ Returns
134
+ -------
135
+ (error_no, result) where error_no is 0 on success and result is a
136
+ list of 2*nv lists:
137
+ [t_0, t_1, …, t_{nv-1}, v_0, v_1, …, v_{nv-1}]
138
+ where each t_i / v_i is a Python list of floats.
139
+ """
140
+
141
+ try:
142
+ data = _read_file(filename)
143
+ except FileNotFoundError:
144
+ return 2, [] # ERROR_FILE_NOT_FOUND
145
+ except Exception:
146
+ return 1, [] # ERROR_UNEXPECTED_END_OF_FILE
147
+ nv = len(vi)
148
+ bs_list = list(byte_sizes)
149
+ vit, nti = _insert_ti(list(vi), ti)
150
+ nvt = len(vit) # = nv + 1
151
+
152
+ # ── byte-order detection ─────────────────────────────────────────────────
153
+ endian = '<' if struct.unpack_from('<H', data, bin_offset + 2)[0] == 4660 else '>'
154
+
155
+ # ── numpy array of byte sizes (used in vectorised pass 2) ────────────────
156
+ bs_arr = np.array(bs_list, dtype=np.int32) # (n_sensors,)
157
+ vit_arr = np.array(vit, dtype=np.intp) # (nvt,)
158
+
159
+ # ── chunk-size lookup table (for pass 1) ─────────────────────────────────
160
+ chunk_lut = _build_chunk_lut(n_state_bytes, n_sensors, bs_list)
161
+
162
+ # ── PASS 1: locate cycle boundaries ──────────────────────────────────────
163
+ # For each cycle record (state_bytes + data_chunk + separator):
164
+ # * record the file position of the state bytes
165
+ # * compute the data-chunk size using the lookup table
166
+ # Cost: O(n_cycles × n_state_bytes) pure-Python ops — cheap.
167
+
168
+ min_offset_value = -2 if return_nans else -1
169
+ file_size = len(data)
170
+ pos = bin_offset + 17 # skip known cycle
171
+ write_data = not bool(skip_initial_line)
172
+
173
+ state_positions = [] # file pos of state bytes for each cycle
174
+ chunk_sizes = [] # data-chunk byte count for each cycle
175
+ write_flags = [] # whether to emit output for each cycle
176
+
177
+ while pos < file_size:
178
+ if pos + n_state_bytes > file_size:
179
+ break
180
+
181
+ # Chunksize via LUT: O(n_state_bytes) lookups
182
+ chunksize = 0
183
+ for bpos in range(n_state_bytes):
184
+ chunksize += chunk_lut[bpos][data[pos + bpos]]
185
+
186
+ state_positions.append(pos)
187
+ chunk_sizes.append(chunksize)
188
+ write_flags.append(write_data)
189
+
190
+ pos += n_state_bytes + chunksize + 1
191
+ write_data = True
192
+
193
+ n_cycles = len(state_positions)
194
+ if n_cycles == 0:
195
+ return 0, [[] for _ in range(nv)] + [[] for _ in range(nv)]
196
+
197
+ # ── PASS 2: vectorised processing of all cycles at once ──────────────────
198
+ data_u8 = np.frombuffer(data, dtype=np.uint8)
199
+ sp_arr = np.array(state_positions, dtype=np.intp) # (n_cycles,)
200
+ cstart = sp_arr + n_state_bytes # chunk start positions
201
+
202
+ # Extract state bytes for ALL cycles: shape (n_cycles, n_state_bytes)
203
+ sb_idx = sp_arr[:, np.newaxis] + np.arange(n_state_bytes, dtype=np.intp)
204
+ all_sb = data_u8[sb_idx] # uint8, (n_cycles, n_state_bytes)
205
+
206
+ # Decode 2-bit fields: (n_cycles, n_state_bytes, 4) → (n_cycles, n_sensors)
207
+ all_fields = ((all_sb[:, :, np.newaxis] >> _SHIFTS) & 3) # int, (n_cycles, n_state_bytes, 4)
208
+ all_fields = all_fields.reshape(n_cycles, n_state_bytes * 4)[:, :n_sensors]
209
+
210
+ # Compute byte offsets within the data chunk for every sensor in every cycle.
211
+ # UPDATED → cumulative offset; SAME → -1; NOTSET → -2.
212
+ upd_mask = (all_fields == UPDATED) # bool, (n_cycles, n_sensors)
213
+ w_bs = np.where(upd_mask, bs_arr[np.newaxis, :], 0) # int32, (n_cycles, n_sensors)
214
+ cum_bs = np.cumsum(w_bs, axis=1) # int32, (n_cycles, n_sensors)
215
+ all_off = np.where(upd_mask, cum_bs - w_bs, np.int32(-1)) # (n_cycles, n_sensors)
216
+ all_off = np.where(all_fields == NOTSET, np.int32(-2), all_off)
217
+
218
+ # Offsets for only the wanted sensors: (n_cycles, nvt)
219
+ wanted_off = all_off[:, vit_arr] # int32, (n_cycles, nvt)
220
+
221
+ # ── helper: read all values for one column of wanted_off ─────────────────
222
+ def _read_col(col: int) -> np.ndarray:
223
+ """
224
+ Return float64 array (n_cycles,) for wanted sensor at column *col*.
225
+ UPDATED cycles → actual data value.
226
+ SAME cycles → carry-forward of last UPDATED value (numpy ffill).
227
+ NOTSET cycles → NaN (or FILLVALUE if return_nans).
228
+ """
229
+ sidx = vit[col]
230
+ bs = bs_list[sidx]
231
+ offsets = wanted_off[:, col] # int32, (n_cycles,)
232
+ vals = np.full(n_cycles, np.nan, dtype=np.float64)
233
+
234
+ upd = np.where(offsets >= 0)[0] # indices of UPDATED cycles
235
+ if upd.size:
236
+ abs_pos = cstart[upd] + offsets[upd].astype(np.intp)
237
+
238
+ if bs == 1:
239
+ raw = data_u8[abs_pos].view(np.int8).astype(np.float64)
240
+ vals[upd] = raw
241
+ else:
242
+ # Gather bs consecutive bytes for each updated cycle,
243
+ # reinterpret as the sensor's native type.
244
+ bidx = abs_pos[:, np.newaxis] + np.arange(bs, dtype=np.intp)
245
+ raw = data_u8[bidx].tobytes() # flat byte buffer
246
+ dt = np.dtype(endian + _FMT_CHAR[bs])
247
+ vals[upd] = np.frombuffer(raw, dtype=dt)
248
+
249
+ # Forward-fill: propagate each UPDATED value into subsequent SAME slots.
250
+ # Classic numpy trick: build an index array that "sticks" at the last
251
+ # non-NaN position, then use it to index vals.
252
+ has_val = ~np.isnan(vals)
253
+ ff_idx = np.where(has_val, np.arange(n_cycles, dtype=np.intp), np.intp(0))
254
+ np.maximum.accumulate(ff_idx, out=ff_idx)
255
+ vals = vals[ff_idx]
256
+
257
+ # NOTSET cycles: mark as FILLVALUE (if return_nans) or leave as
258
+ # carried-forward value (which will be excluded by the mask below).
259
+ if return_nans:
260
+ vals[offsets == np.int32(-2)] = FILLVALUE
261
+
262
+ return vals
263
+
264
+ # ── read time and sensor values ───────────────────────────────────────────
265
+ t_vals = _read_col(nti) # (n_cycles,) float64
266
+ write_arr = np.array(write_flags, dtype=bool)
267
+
268
+ result_t = [None] * nv
269
+ result_v = [None] * nv
270
+
271
+ for col in range(nvt):
272
+ if col == nti:
273
+ continue
274
+ j = col - (1 if col > nti else 0) # output index in result_t/v
275
+
276
+ v_vals = _read_col(col)
277
+ v_off = wanted_off[:, col]
278
+
279
+ # Include this cycle in output if:
280
+ # • write_flags says so (skip_initial_line handling)
281
+ # • sensor has a valid offset (UPDATED or SAME; also NOTSET if return_nans)
282
+ include = (v_off >= min_offset_value)
283
+ mask = write_arr & include
284
+
285
+ if max_values_to_read > 0:
286
+ keep = np.where(mask)[0]
287
+ if keep.size > max_values_to_read:
288
+ mask = np.zeros(n_cycles, dtype=bool)
289
+ mask[keep[:max_values_to_read]] = True
290
+
291
+ result_t[j] = t_vals[mask].tolist()
292
+ result_v[j] = v_vals[mask].tolist()
293
+
294
+ return 0, [result_t[j] for j in range(nv)] + [result_v[j] for j in range(nv)]
Binary file
Binary file
Binary file
Binary file