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.
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/PKG-INFO +27 -4
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/README.md +24 -0
- dbdreader-0.6.2/dbdreader/_dbdreader.py +312 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/dbdreader.py +1086 -771
- dbdreader-0.6.2/dbdreader/decompress.py +339 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/PKG-INFO +27 -4
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/SOURCES.txt +1 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/requires.txt +0 -1
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/decompress.c +13 -5
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/pyproject.toml +8 -7
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/requirements.txt +0 -1
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/tests/test_dbdreader.py +0 -7
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/tests/test_decompress.py +35 -20
- dbdreader-0.6.0.dev1/dbdreader/decompress.py +0 -247
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/COPYING +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/COPYRIGHT +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/INSTALL.rst +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/LICENSE +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/MANIFEST.in +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/__init__.py +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.dcd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.ebd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.ecd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.mcg +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600000.mlg +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600001.dbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600001.dcd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600001.ebd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/01600001.ecd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/02380107.ecd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/02380108.ecd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/02450133.tcd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/02450137.tcd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-203-00-000.SBD +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-203-00-000.TBD +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-000.dbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-000.ebd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-000.sbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-000.tbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-001.sbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-001.tbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-002.sbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/amadeus-2014-204-05-002.tbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/ammonite-2008-028-01-000.mbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/06a36d4e.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/06a36d4e.ccc +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/093bd5ed.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/0f682cb2.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/39b2c12e.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/468fd1be.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/5fbf5947.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/61b1780f.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/6f9a08d3.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/813b137d.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/8d31d0c8.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/92610b65.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/c4ec741e.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/d6f44165.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/daad1b20.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/daad1b20.ccc +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac/dc76ebd5.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/cac_missing/d6f44165.cac +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/dbd2asc_output.txt +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/electa-2023-143-00-050.sbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/electa-2023-143-00-050.tbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/empty-2014-204-05-000.dbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/hal_1002-2024-183-4-4.sbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/hal_1002-2024-183-4-4.tbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/hal_1002-2024-183-4-6.tbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/invalid_encoding-2014-204-05-000.dbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/sebastian-2014-204-05-000.dbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/sebastian-2014-204-05-000.ebd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/sebastian-2014-204-05-001.dbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/sebastian-2014-204-05-001.ebd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/unit_887-2021-321-3-0.sbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/data/unit_887-2021-321-3-0.tbd +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader/scripts.py +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/dependency_links.txt +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/entry_points.txt +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/dbdreader.egg-info/top_level.txt +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/examples/read_multiple_dbd_files.py +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/examples/read_single_dbd_file.py +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/dbdreader.c +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/include/dbdreader.h +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/include/decompress.h +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/extension/py_dbdreader.c +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/lz4/include/lz4.h +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/lz4/lz4.c +0 -0
- {dbdreader-0.6.0.dev1 → dbdreader-0.6.2}/setup.cfg +0 -0
- {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.
|
|
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
|
[](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)]
|