pyelectrochemreader 0.1.0__py3-none-any.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.
- pyelectrochemreader/__init__.py +8 -0
- pyelectrochemreader/_biologic_columns.py +143 -0
- pyelectrochemreader/_neware_columns.py +73 -0
- pyelectrochemreader/_nrbf.py +418 -0
- pyelectrochemreader/arbin.py +324 -0
- pyelectrochemreader/biologic.py +101 -0
- pyelectrochemreader/core.py +131 -0
- pyelectrochemreader/licenses/GPL-3.0.txt +674 -0
- pyelectrochemreader/licenses/MDBTools-LGPL-2.0.txt +481 -0
- pyelectrochemreader/licenses/NewareNDA.txt +11 -0
- pyelectrochemreader/neware.py +352 -0
- pyelectrochemreader/nova.py +122 -0
- pyelectrochemreader/py.typed +0 -0
- pyelectrochemreader/reader.py +66 -0
- pyelectrochemreader/text.py +116 -0
- pyelectrochemreader-0.1.0.dist-info/METADATA +153 -0
- pyelectrochemreader-0.1.0.dist-info/RECORD +21 -0
- pyelectrochemreader-0.1.0.dist-info/WHEEL +5 -0
- pyelectrochemreader-0.1.0.dist-info/licenses/LICENSE +674 -0
- pyelectrochemreader-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +29 -0
- pyelectrochemreader-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Read electrochemical files using only Python and NumPy."""
|
|
2
|
+
|
|
3
|
+
from .core import Dataset, Experiment, ReadError, UnsupportedFormatError
|
|
4
|
+
from .reader import read, supported_formats
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["read", "supported_formats", "Dataset", "Experiment", "ReadError",
|
|
8
|
+
"UnsupportedFormatError"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Bio-Logic column layouts, adapted from Galvani (modified 2026-11-02).
|
|
2
|
+
|
|
3
|
+
SPDX-FileCopyrightText: 2013-2020 Christopher Kerr, "bcolsen"
|
|
4
|
+
SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
"""
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
VMPdata_colID_dtype_map = {
|
|
9
|
+
4: ("time/s", "<f8"),
|
|
10
|
+
5: ("control/V/mA", "<f4"),
|
|
11
|
+
6: ("Ewe/V", "<f4"),
|
|
12
|
+
7: ("dq/mA.h", "<f8"),
|
|
13
|
+
8: ("I/mA", "<f4"), # 8 is either I or <I> ??
|
|
14
|
+
9: ("Ece/V", "<f4"),
|
|
15
|
+
11: ("<I>/mA", "<f8"),
|
|
16
|
+
13: ("(Q-Qo)/mA.h", "<f8"),
|
|
17
|
+
16: ("Analog IN 1/V", "<f4"),
|
|
18
|
+
17: ("Analog IN 2/V", "<f4"), # Probably column 18 is Analog IN 3/V, if anyone hits this error in the future # noqa: E501
|
|
19
|
+
19: ("control/V", "<f4"),
|
|
20
|
+
20: ("control/mA", "<f4"),
|
|
21
|
+
23: ("dQ/mA.h", "<f8"), # Same as 7?
|
|
22
|
+
24: ("cycle number", "<f8"),
|
|
23
|
+
26: ("Rapp/Ohm", "<f4"),
|
|
24
|
+
27: ("Ewe-Ece/V", "<f4"),
|
|
25
|
+
32: ("freq/Hz", "<f4"),
|
|
26
|
+
33: ("|Ewe|/V", "<f4"),
|
|
27
|
+
34: ("|I|/A", "<f4"),
|
|
28
|
+
35: ("Phase(Z)/deg", "<f4"),
|
|
29
|
+
36: ("|Z|/Ohm", "<f4"),
|
|
30
|
+
37: ("Re(Z)/Ohm", "<f4"),
|
|
31
|
+
38: ("-Im(Z)/Ohm", "<f4"),
|
|
32
|
+
39: ("I Range", "<u2"),
|
|
33
|
+
69: ("R/Ohm", "<f4"),
|
|
34
|
+
70: ("P/W", "<f4"),
|
|
35
|
+
74: ("|Energy|/W.h", "<f8"),
|
|
36
|
+
75: ("Analog OUT/V", "<f4"),
|
|
37
|
+
76: ("<I>/mA", "<f4"),
|
|
38
|
+
77: ("<Ewe>/V", "<f4"),
|
|
39
|
+
78: ("Cs-2/µF-2", "<f4"),
|
|
40
|
+
96: ("|Ece|/V", "<f4"),
|
|
41
|
+
98: ("Phase(Zce)/deg", "<f4"),
|
|
42
|
+
99: ("|Zce|/Ohm", "<f4"),
|
|
43
|
+
100: ("Re(Zce)/Ohm", "<f4"),
|
|
44
|
+
101: ("-Im(Zce)/Ohm", "<f4"),
|
|
45
|
+
123: ("Energy charge/W.h", "<f8"),
|
|
46
|
+
124: ("Energy discharge/W.h", "<f8"),
|
|
47
|
+
125: ("Capacitance charge/µF", "<f8"),
|
|
48
|
+
126: ("Capacitance discharge/µF", "<f8"),
|
|
49
|
+
131: ("Ns", "<u2"),
|
|
50
|
+
163: ("|Estack|/V", "<f4"),
|
|
51
|
+
168: ("Rcmp/Ohm", "<f4"),
|
|
52
|
+
169: ("Cs/µF", "<f4"),
|
|
53
|
+
172: ("Cp/µF", "<f4"),
|
|
54
|
+
173: ("Cp-2/µF-2", "<f4"),
|
|
55
|
+
174: ("<Ewe>/V", "<f4"),
|
|
56
|
+
178: ("(Q-Qo)/C", "<f4"),
|
|
57
|
+
179: ("dQ/C", "<f4"),
|
|
58
|
+
182: ("step time/s", "<f8"),
|
|
59
|
+
211: ("Q charge/discharge/mA.h", "<f8"),
|
|
60
|
+
212: ("half cycle", "<u4"),
|
|
61
|
+
213: ("z cycle", "<u4"),
|
|
62
|
+
217: ("THD Ewe/%", "<f4"),
|
|
63
|
+
218: ("THD I/%", "<f4"),
|
|
64
|
+
220: ("NSD Ewe/%", "<f4"),
|
|
65
|
+
221: ("NSD I/%", "<f4"),
|
|
66
|
+
223: ("NSR Ewe/%", "<f4"),
|
|
67
|
+
224: ("NSR I/%", "<f4"),
|
|
68
|
+
230: ("|Ewe h2|/V", "<f4"),
|
|
69
|
+
231: ("|Ewe h3|/V", "<f4"),
|
|
70
|
+
232: ("|Ewe h4|/V", "<f4"),
|
|
71
|
+
233: ("|Ewe h5|/V", "<f4"),
|
|
72
|
+
234: ("|Ewe h6|/V", "<f4"),
|
|
73
|
+
235: ("|Ewe h7|/V", "<f4"),
|
|
74
|
+
236: ("|I h2|/A", "<f4"),
|
|
75
|
+
237: ("|I h3|/A", "<f4"),
|
|
76
|
+
238: ("|I h4|/A", "<f4"),
|
|
77
|
+
239: ("|I h5|/A", "<f4"),
|
|
78
|
+
240: ("|I h6|/A", "<f4"),
|
|
79
|
+
241: ("|I h7|/A", "<f4"),
|
|
80
|
+
242: ("|E2|/V", "<f4"),
|
|
81
|
+
271: ("Phase(Z1) / deg", "<f4"),
|
|
82
|
+
272: ("Phase(Z2) / deg", "<f4"),
|
|
83
|
+
301: ("|Z1|/Ohm", "<f4"),
|
|
84
|
+
302: ("|Z2|/Ohm", "<f4"),
|
|
85
|
+
331: ("Re(Z1)/Ohm", "<f4"),
|
|
86
|
+
332: ("Re(Z2)/Ohm", "<f4"),
|
|
87
|
+
361: ("-Im(Z1)/Ohm", "<f4"),
|
|
88
|
+
362: ("-Im(Z2)/Ohm", "<f4"),
|
|
89
|
+
391: ("<E1>/V", "<f4"),
|
|
90
|
+
392: ("<E2>/V", "<f4"),
|
|
91
|
+
422: ("Phase(Zstack)/deg", "<f4"),
|
|
92
|
+
423: ("|Zstack|/Ohm", "<f4"),
|
|
93
|
+
424: ("Re(Zstack)/Ohm", "<f4"),
|
|
94
|
+
425: ("-Im(Zstack)/Ohm", "<f4"),
|
|
95
|
+
426: ("<Estack>/V", "<f4"),
|
|
96
|
+
430: ("Phase(Zwe-ce)/deg", "<f4"),
|
|
97
|
+
431: ("|Zwe-ce|/Ohm", "<f4"),
|
|
98
|
+
432: ("Re(Zwe-ce)/Ohm", "<f4"),
|
|
99
|
+
433: ("-Im(Zwe-ce)/Ohm", "<f4"),
|
|
100
|
+
434: ("(Q-Qo)/C", "<f4"),
|
|
101
|
+
435: ("dQ/C", "<f4"),
|
|
102
|
+
438: ("step time/s", "<f8"),
|
|
103
|
+
441: ("<Ecv>/V", "<f4"),
|
|
104
|
+
462: ("Temperature/°C", "<f4"),
|
|
105
|
+
467: ("Q charge/discharge/mA.h", "<f8"),
|
|
106
|
+
468: ("half cycle", "<u4"),
|
|
107
|
+
469: ("z cycle", "<u4"),
|
|
108
|
+
471: ("<Ece>/V", "<f4"),
|
|
109
|
+
473: ("THD Ewe/%", "<f4"),
|
|
110
|
+
474: ("THD I/%", "<f4"),
|
|
111
|
+
476: ("NSD Ewe/%", "<f4"),
|
|
112
|
+
477: ("NSD I/%", "<f4"),
|
|
113
|
+
479: ("NSR Ewe/%", "<f4"),
|
|
114
|
+
480: ("NSR I/%", "<f4"),
|
|
115
|
+
486: ("|Ewe h2|/V", "<f4"),
|
|
116
|
+
487: ("|Ewe h3|/V", "<f4"),
|
|
117
|
+
488: ("|Ewe h4|/V", "<f4"),
|
|
118
|
+
489: ("|Ewe h5|/V", "<f4"),
|
|
119
|
+
490: ("|Ewe h6|/V", "<f4"),
|
|
120
|
+
491: ("|Ewe h7|/V", "<f4"),
|
|
121
|
+
492: ("|I h2|/A", "<f4"),
|
|
122
|
+
493: ("|I h3|/A", "<f4"),
|
|
123
|
+
494: ("|I h4|/A", "<f4"),
|
|
124
|
+
495: ("|I h5|/A", "<f4"),
|
|
125
|
+
496: ("|I h6|/A", "<f4"),
|
|
126
|
+
497: ("|I h7|/A", "<f4"),
|
|
127
|
+
498: ("Q charge/mA.h", "<f8"),
|
|
128
|
+
499: ("Q discharge/mA.h", "<f8"),
|
|
129
|
+
500: ("step time/s", "<f8"),
|
|
130
|
+
501: ("Efficiency/%", "<f8"),
|
|
131
|
+
502: ("Capacity/mA.h", "<f8"),
|
|
132
|
+
505: ("Rdc/Ohm", "<f4"),
|
|
133
|
+
509: ("Acir/Dcir Control", "<u1"),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
VMPdata_colID_flag_map = {
|
|
137
|
+
1: ("mode", 0x03, np.uint8),
|
|
138
|
+
2: ("ox/red", 0x04, np.bool_),
|
|
139
|
+
3: ("error", 0x08, np.bool_),
|
|
140
|
+
21: ("control changes", 0x10, np.bool_),
|
|
141
|
+
31: ("Ns changes", 0x20, np.bool_),
|
|
142
|
+
65: ("counter inc.", 0x80, np.bool_),
|
|
143
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Neware scales and states from NewareNDA (modified 2026-11-02).
|
|
2
|
+
Copyright (c) 2022-2024 SES AI Corporation. BSD-3-Clause; see licenses/NewareNDA.txt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
state_dict = {
|
|
6
|
+
1: 'CC_Chg',
|
|
7
|
+
2: 'CC_DChg',
|
|
8
|
+
3: 'CV_Chg',
|
|
9
|
+
4: 'Rest',
|
|
10
|
+
5: 'Cycle',
|
|
11
|
+
7: 'CCCV_Chg',
|
|
12
|
+
8: 'CP_DChg',
|
|
13
|
+
9: 'CP_Chg',
|
|
14
|
+
10: 'CR_DChg',
|
|
15
|
+
13: 'Pause',
|
|
16
|
+
16: 'Pulse',
|
|
17
|
+
17: 'SIM',
|
|
18
|
+
19: 'CV_DChg',
|
|
19
|
+
20: 'CCCV_DChg',
|
|
20
|
+
21: 'Control',
|
|
21
|
+
22: 'OCV',
|
|
22
|
+
26: 'CPCV_DChg',
|
|
23
|
+
27: 'CPCV_Chg'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Define field scaling based on instrument Range setting
|
|
27
|
+
multiplier_dict = {
|
|
28
|
+
-100000000: 10,
|
|
29
|
+
-200000: 1e-2,
|
|
30
|
+
-100000: 1e-2,
|
|
31
|
+
-60000: 1e-2,
|
|
32
|
+
-30000: 1e-2,
|
|
33
|
+
-50000: 1e-2,
|
|
34
|
+
-20000: 1e-2,
|
|
35
|
+
-12000: 1e-2,
|
|
36
|
+
-10000: 1e-2,
|
|
37
|
+
-6000: 1e-2,
|
|
38
|
+
-5000: 1e-2,
|
|
39
|
+
-3000: 1e-2,
|
|
40
|
+
-2000: 1e-2,
|
|
41
|
+
-1000: 1e-2,
|
|
42
|
+
-500: 1e-3,
|
|
43
|
+
-100: 1e-3,
|
|
44
|
+
-50: 1e-4,
|
|
45
|
+
-25: 1e-4,
|
|
46
|
+
-20: 1e-4,
|
|
47
|
+
-10: 1e-4,
|
|
48
|
+
-5: 1e-5,
|
|
49
|
+
-2: 1e-5,
|
|
50
|
+
-1: 1e-5,
|
|
51
|
+
0: 0,
|
|
52
|
+
1: 1e-4,
|
|
53
|
+
2: 1e-4,
|
|
54
|
+
5: 1e-4,
|
|
55
|
+
10: 1e-3,
|
|
56
|
+
20: 1e-3,
|
|
57
|
+
50: 1e-3,
|
|
58
|
+
100: 1e-2,
|
|
59
|
+
200: 1e-2,
|
|
60
|
+
250: 1e-2,
|
|
61
|
+
500: 1e-2,
|
|
62
|
+
1000: 1e-1,
|
|
63
|
+
6000: 1e-1,
|
|
64
|
+
10000: 1e-1,
|
|
65
|
+
12000: 1e-1,
|
|
66
|
+
20000: 1e-1,
|
|
67
|
+
30000: 1e-1,
|
|
68
|
+
40000: 1e-1,
|
|
69
|
+
50000: 1e-1,
|
|
70
|
+
60000: 1e-1,
|
|
71
|
+
100000: 1e-1,
|
|
72
|
+
200000: 1e-1,
|
|
73
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Small, safe decoder for the records used by NOVA BinaryFormatter files.
|
|
2
|
+
|
|
3
|
+
This module decodes the wire representation into dictionaries and arrays. It
|
|
4
|
+
does not load .NET assemblies or instantiate any of the serialized classes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# SPDX-FileCopyrightText: 2026 OpenAI
|
|
8
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NRBFError(ValueError):
|
|
16
|
+
"""Raised when an invalid or unsupported NRBF stream is encountered."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_RECORD_HEADER = 0
|
|
20
|
+
_RECORD_CLASS_WITH_ID = 1
|
|
21
|
+
_RECORD_SYSTEM_CLASS = 4
|
|
22
|
+
_RECORD_CLASS = 5
|
|
23
|
+
_RECORD_STRING = 6
|
|
24
|
+
_RECORD_BINARY_ARRAY = 7
|
|
25
|
+
_RECORD_PRIMITIVE = 8
|
|
26
|
+
_RECORD_REFERENCE = 9
|
|
27
|
+
_RECORD_NULL = 10
|
|
28
|
+
_RECORD_END = 11
|
|
29
|
+
_RECORD_LIBRARY = 12
|
|
30
|
+
_RECORD_NULL_256 = 13
|
|
31
|
+
_RECORD_NULL_MULTIPLE = 14
|
|
32
|
+
_RECORD_PRIMITIVE_ARRAY = 15
|
|
33
|
+
_RECORD_OBJECT_ARRAY = 16
|
|
34
|
+
_RECORD_STRING_ARRAY = 17
|
|
35
|
+
|
|
36
|
+
_TYPE_PRIMITIVE = 0
|
|
37
|
+
_TYPE_SYSTEM_CLASS = 3
|
|
38
|
+
_TYPE_CLASS = 4
|
|
39
|
+
_TYPE_PRIMITIVE_ARRAY = 7
|
|
40
|
+
|
|
41
|
+
_ARRAY_OFFSET_TYPES = (3, 4, 5)
|
|
42
|
+
|
|
43
|
+
# These value-type arrays dominate the size of high-rate NOVA files. NOVA
|
|
44
|
+
# also serializes the corresponding calculated signal arrays, which are what
|
|
45
|
+
# its own public API exposes. Decode the records but do not retain them.
|
|
46
|
+
_DISCARDED_ARRAY_CLASSES = (
|
|
47
|
+
"EcoChemie.Autolab.Adc164RawSample",
|
|
48
|
+
"EcoChemie.Autolab.Adc164RawCrSample",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_NUMPY_PRIMITIVES = {
|
|
52
|
+
1: "?", # Boolean
|
|
53
|
+
2: "u1", # Byte
|
|
54
|
+
6: "<f8", # Double
|
|
55
|
+
7: "<i2", # Int16
|
|
56
|
+
8: "<i4", # Int32
|
|
57
|
+
9: "<i8", # Int64
|
|
58
|
+
10: "i1", # SByte
|
|
59
|
+
11: "<f4", # Single
|
|
60
|
+
12: "<i8", # TimeSpan
|
|
61
|
+
13: "<u8", # DateTime
|
|
62
|
+
14: "<u2", # UInt16
|
|
63
|
+
15: "<u4", # UInt32
|
|
64
|
+
16: "<u8", # UInt64
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class NRBFParser:
|
|
69
|
+
"""Decode one NRBF stream starting at *offset* in a bytes-like object."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, data, offset=0):
|
|
72
|
+
self.data = memoryview(data)
|
|
73
|
+
self.offset = offset
|
|
74
|
+
self.objects = {}
|
|
75
|
+
self.libraries = {}
|
|
76
|
+
self.class_definitions = {}
|
|
77
|
+
|
|
78
|
+
def parse(self):
|
|
79
|
+
"""Return the unresolved root object and stop just after MessageEnd."""
|
|
80
|
+
if self._u8() != _RECORD_HEADER:
|
|
81
|
+
raise self._error("stream does not start with an NRBF header")
|
|
82
|
+
root_id = self._i32()
|
|
83
|
+
self._i32() # header id
|
|
84
|
+
major = self._i32()
|
|
85
|
+
minor = self._i32()
|
|
86
|
+
if (major, minor) != (1, 0):
|
|
87
|
+
raise self._error("unsupported NRBF version %d.%d" % (major, minor))
|
|
88
|
+
|
|
89
|
+
while self.offset < len(self.data):
|
|
90
|
+
record_offset = self.offset
|
|
91
|
+
record = self._u8()
|
|
92
|
+
if record == _RECORD_END:
|
|
93
|
+
return self.objects.get(root_id)
|
|
94
|
+
if record == _RECORD_LIBRARY:
|
|
95
|
+
self._read_library()
|
|
96
|
+
elif record == _RECORD_CLASS:
|
|
97
|
+
self._read_class(False)
|
|
98
|
+
elif record == _RECORD_SYSTEM_CLASS:
|
|
99
|
+
self._read_class(True)
|
|
100
|
+
elif record == _RECORD_CLASS_WITH_ID:
|
|
101
|
+
self._read_class_with_id()
|
|
102
|
+
elif record == _RECORD_STRING:
|
|
103
|
+
self._read_string()
|
|
104
|
+
elif record == _RECORD_PRIMITIVE_ARRAY:
|
|
105
|
+
self._read_primitive_array()
|
|
106
|
+
elif record == _RECORD_OBJECT_ARRAY:
|
|
107
|
+
self._read_object_array()
|
|
108
|
+
elif record == _RECORD_STRING_ARRAY:
|
|
109
|
+
self._read_object_array()
|
|
110
|
+
elif record == _RECORD_BINARY_ARRAY:
|
|
111
|
+
self._read_binary_array()
|
|
112
|
+
else:
|
|
113
|
+
raise self._error(
|
|
114
|
+
"unsupported top-level record %d at offset %d"
|
|
115
|
+
% (record, record_offset)
|
|
116
|
+
)
|
|
117
|
+
raise self._error("NRBF stream has no MessageEnd record")
|
|
118
|
+
|
|
119
|
+
def resolve(self, value):
|
|
120
|
+
"""Resolve one MemberReference, without recursively copying a graph."""
|
|
121
|
+
seen = set()
|
|
122
|
+
while isinstance(value, dict) and set(value) == {"__ref__"}:
|
|
123
|
+
object_id = value["__ref__"]
|
|
124
|
+
if object_id in seen:
|
|
125
|
+
return None
|
|
126
|
+
seen.add(object_id)
|
|
127
|
+
value = self.objects.get(object_id)
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
def _error(self, message):
|
|
131
|
+
return NRBFError("%s (offset %d)" % (message, self.offset))
|
|
132
|
+
|
|
133
|
+
def _read(self, size):
|
|
134
|
+
end = self.offset + size
|
|
135
|
+
if size < 0 or end > len(self.data):
|
|
136
|
+
raise self._error("unexpected end of file")
|
|
137
|
+
result = self.data[self.offset:end]
|
|
138
|
+
self.offset = end
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
def _unpack(self, fmt):
|
|
142
|
+
size = struct.calcsize(fmt)
|
|
143
|
+
return struct.unpack_from(fmt, self._read(size))[0]
|
|
144
|
+
|
|
145
|
+
def _u8(self):
|
|
146
|
+
return self._unpack("<B")
|
|
147
|
+
|
|
148
|
+
def _i8(self):
|
|
149
|
+
return self._unpack("<b")
|
|
150
|
+
|
|
151
|
+
def _u16(self):
|
|
152
|
+
return self._unpack("<H")
|
|
153
|
+
|
|
154
|
+
def _i16(self):
|
|
155
|
+
return self._unpack("<h")
|
|
156
|
+
|
|
157
|
+
def _u32(self):
|
|
158
|
+
return self._unpack("<I")
|
|
159
|
+
|
|
160
|
+
def _i32(self):
|
|
161
|
+
return self._unpack("<i")
|
|
162
|
+
|
|
163
|
+
def _u64(self):
|
|
164
|
+
return self._unpack("<Q")
|
|
165
|
+
|
|
166
|
+
def _i64(self):
|
|
167
|
+
return self._unpack("<q")
|
|
168
|
+
|
|
169
|
+
def _f32(self):
|
|
170
|
+
return self._unpack("<f")
|
|
171
|
+
|
|
172
|
+
def _f64(self):
|
|
173
|
+
return self._unpack("<d")
|
|
174
|
+
|
|
175
|
+
def _string(self):
|
|
176
|
+
length = 0
|
|
177
|
+
shift = 0
|
|
178
|
+
while True:
|
|
179
|
+
byte = self._u8()
|
|
180
|
+
length |= (byte & 0x7f) << shift
|
|
181
|
+
if not byte & 0x80:
|
|
182
|
+
break
|
|
183
|
+
shift += 7
|
|
184
|
+
if shift > 35:
|
|
185
|
+
raise self._error("invalid length-prefixed string")
|
|
186
|
+
try:
|
|
187
|
+
return self._read(length).tobytes().decode("utf-8")
|
|
188
|
+
except UnicodeDecodeError as exc:
|
|
189
|
+
raise self._error("invalid UTF-8 string: %s" % exc)
|
|
190
|
+
|
|
191
|
+
def _primitive(self, primitive_type):
|
|
192
|
+
readers = {
|
|
193
|
+
1: self._u8,
|
|
194
|
+
2: self._u8,
|
|
195
|
+
6: self._f64,
|
|
196
|
+
7: self._i16,
|
|
197
|
+
8: self._i32,
|
|
198
|
+
9: self._i64,
|
|
199
|
+
10: self._i8,
|
|
200
|
+
11: self._f32,
|
|
201
|
+
12: self._i64,
|
|
202
|
+
13: self._u64,
|
|
203
|
+
14: self._u16,
|
|
204
|
+
15: self._u32,
|
|
205
|
+
16: self._u64,
|
|
206
|
+
}
|
|
207
|
+
if primitive_type == 1:
|
|
208
|
+
return bool(self._u8())
|
|
209
|
+
if primitive_type == 3: # Char is UTF-8 encoded by BinaryFormatter.
|
|
210
|
+
first = self._u8()
|
|
211
|
+
if first < 0x80:
|
|
212
|
+
return chr(first)
|
|
213
|
+
count = 1 if first < 0xe0 else 2 if first < 0xf0 else 3
|
|
214
|
+
raw = bytes([first] + [self._u8() for unused in range(count)])
|
|
215
|
+
return raw.decode("utf-8")
|
|
216
|
+
if primitive_type == 5: # Decimal is written as a string.
|
|
217
|
+
return self._string()
|
|
218
|
+
try:
|
|
219
|
+
return readers[primitive_type]()
|
|
220
|
+
except KeyError:
|
|
221
|
+
raise self._error("unsupported primitive type %d" % primitive_type)
|
|
222
|
+
|
|
223
|
+
def _read_library(self):
|
|
224
|
+
library_id = self._i32()
|
|
225
|
+
self.libraries[library_id] = self._string()
|
|
226
|
+
|
|
227
|
+
def _read_class_info(self):
|
|
228
|
+
object_id = self._i32()
|
|
229
|
+
name = self._string()
|
|
230
|
+
count = self._i32()
|
|
231
|
+
if count < 0 or count > 100000:
|
|
232
|
+
raise self._error("invalid class member count %d" % count)
|
|
233
|
+
names = [self._string() for unused in range(count)]
|
|
234
|
+
return object_id, name, names
|
|
235
|
+
|
|
236
|
+
def _read_type_info(self, count):
|
|
237
|
+
binary_types = [self._u8() for unused in range(count)]
|
|
238
|
+
additional = []
|
|
239
|
+
for binary_type in binary_types:
|
|
240
|
+
if binary_type in (_TYPE_PRIMITIVE, _TYPE_PRIMITIVE_ARRAY):
|
|
241
|
+
additional.append(self._u8())
|
|
242
|
+
elif binary_type == _TYPE_SYSTEM_CLASS:
|
|
243
|
+
additional.append(self._string())
|
|
244
|
+
elif binary_type == _TYPE_CLASS:
|
|
245
|
+
additional.append((self._string(), self._i32()))
|
|
246
|
+
elif binary_type in (1, 2, 5, 6):
|
|
247
|
+
additional.append(None)
|
|
248
|
+
else:
|
|
249
|
+
raise self._error("unsupported binary type %d" % binary_type)
|
|
250
|
+
return binary_types, additional
|
|
251
|
+
|
|
252
|
+
def _read_class(self, system_class):
|
|
253
|
+
object_id, name, names = self._read_class_info()
|
|
254
|
+
binary_types, additional = self._read_type_info(len(names))
|
|
255
|
+
if not system_class:
|
|
256
|
+
self._i32() # library id
|
|
257
|
+
definition = (name, names, binary_types, additional)
|
|
258
|
+
self.class_definitions[object_id] = definition
|
|
259
|
+
obj = self._read_class_value(object_id, definition)
|
|
260
|
+
return obj
|
|
261
|
+
|
|
262
|
+
def _read_class_with_id(self):
|
|
263
|
+
object_id = self._i32()
|
|
264
|
+
metadata_id = self._i32()
|
|
265
|
+
try:
|
|
266
|
+
definition = self.class_definitions[metadata_id]
|
|
267
|
+
except KeyError:
|
|
268
|
+
raise self._error("unknown class metadata id %d" % metadata_id)
|
|
269
|
+
return self._read_class_value(object_id, definition)
|
|
270
|
+
|
|
271
|
+
def _read_class_value(self, object_id, definition):
|
|
272
|
+
name, names, binary_types, additional = definition
|
|
273
|
+
discard = name in _DISCARDED_ARRAY_CLASSES
|
|
274
|
+
if discard:
|
|
275
|
+
for binary_type, extra in zip(binary_types, additional):
|
|
276
|
+
self._read_value(binary_type, extra)
|
|
277
|
+
return None
|
|
278
|
+
values = [
|
|
279
|
+
self._read_value(binary_type, extra)
|
|
280
|
+
for binary_type, extra in zip(binary_types, additional)
|
|
281
|
+
]
|
|
282
|
+
obj = dict(zip(names, values))
|
|
283
|
+
obj["__class__"] = name
|
|
284
|
+
self.objects[object_id] = obj
|
|
285
|
+
return obj
|
|
286
|
+
|
|
287
|
+
def _read_value(self, binary_type, additional):
|
|
288
|
+
if binary_type == _TYPE_PRIMITIVE:
|
|
289
|
+
return self._primitive(additional)
|
|
290
|
+
return self._read_inline()
|
|
291
|
+
|
|
292
|
+
def _read_inline(self):
|
|
293
|
+
record_offset = self.offset
|
|
294
|
+
record = self._u8()
|
|
295
|
+
if record == _RECORD_STRING:
|
|
296
|
+
return self._read_string()
|
|
297
|
+
if record == _RECORD_REFERENCE:
|
|
298
|
+
return {"__ref__": self._i32()}
|
|
299
|
+
if record == _RECORD_NULL:
|
|
300
|
+
return None
|
|
301
|
+
if record == _RECORD_CLASS:
|
|
302
|
+
obj = self._read_class(False)
|
|
303
|
+
return obj
|
|
304
|
+
if record == _RECORD_SYSTEM_CLASS:
|
|
305
|
+
return self._read_class(True)
|
|
306
|
+
if record == _RECORD_CLASS_WITH_ID:
|
|
307
|
+
return self._read_class_with_id()
|
|
308
|
+
if record == _RECORD_PRIMITIVE_ARRAY:
|
|
309
|
+
return self._read_primitive_array()
|
|
310
|
+
if record in (_RECORD_OBJECT_ARRAY, _RECORD_STRING_ARRAY):
|
|
311
|
+
return self._read_object_array()
|
|
312
|
+
if record == _RECORD_BINARY_ARRAY:
|
|
313
|
+
return self._read_binary_array()
|
|
314
|
+
if record == _RECORD_PRIMITIVE:
|
|
315
|
+
return self._primitive(self._u8())
|
|
316
|
+
if record == _RECORD_LIBRARY:
|
|
317
|
+
self._read_library()
|
|
318
|
+
return self._read_inline()
|
|
319
|
+
raise self._error(
|
|
320
|
+
"unsupported inline record %d at offset %d" % (record, record_offset)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _read_string(self):
|
|
324
|
+
object_id = self._i32()
|
|
325
|
+
value = self._string()
|
|
326
|
+
self.objects[object_id] = value
|
|
327
|
+
return value
|
|
328
|
+
|
|
329
|
+
def _read_primitive_array(self):
|
|
330
|
+
object_id = self._i32()
|
|
331
|
+
length = self._i32()
|
|
332
|
+
primitive_type = self._u8()
|
|
333
|
+
value = self._primitive_values(length, primitive_type)
|
|
334
|
+
self.objects[object_id] = value
|
|
335
|
+
return value
|
|
336
|
+
|
|
337
|
+
def _primitive_values(self, length, primitive_type):
|
|
338
|
+
if length < 0:
|
|
339
|
+
raise self._error("negative array length")
|
|
340
|
+
dtype = _NUMPY_PRIMITIVES.get(primitive_type)
|
|
341
|
+
if dtype is None:
|
|
342
|
+
return np.asarray(
|
|
343
|
+
[self._primitive(primitive_type) for unused in range(length)]
|
|
344
|
+
)
|
|
345
|
+
dtype = np.dtype(dtype)
|
|
346
|
+
raw = self._read(length * dtype.itemsize)
|
|
347
|
+
return np.frombuffer(raw, dtype=dtype, count=length)
|
|
348
|
+
|
|
349
|
+
def _read_object_array(self):
|
|
350
|
+
object_id = self._i32()
|
|
351
|
+
length = self._i32()
|
|
352
|
+
value = self._read_array_values(length)
|
|
353
|
+
self.objects[object_id] = value
|
|
354
|
+
return value
|
|
355
|
+
|
|
356
|
+
def _read_array_values(self, length, discard=False):
|
|
357
|
+
if length < 0:
|
|
358
|
+
raise self._error("negative array length")
|
|
359
|
+
value = []
|
|
360
|
+
count = 0
|
|
361
|
+
while count < length:
|
|
362
|
+
record_offset = self.offset
|
|
363
|
+
record = self._u8()
|
|
364
|
+
if record == _RECORD_NULL:
|
|
365
|
+
null_count = 1
|
|
366
|
+
elif record == _RECORD_NULL_256:
|
|
367
|
+
null_count = self._u8()
|
|
368
|
+
elif record == _RECORD_NULL_MULTIPLE:
|
|
369
|
+
null_count = self._i32()
|
|
370
|
+
else:
|
|
371
|
+
self.offset = record_offset
|
|
372
|
+
item = self._read_inline()
|
|
373
|
+
if not discard:
|
|
374
|
+
value.append(item)
|
|
375
|
+
count += 1
|
|
376
|
+
continue
|
|
377
|
+
if null_count < 0 or count + null_count > length:
|
|
378
|
+
raise self._error("invalid null run in array")
|
|
379
|
+
if not discard:
|
|
380
|
+
value.extend([None] * null_count)
|
|
381
|
+
count += null_count
|
|
382
|
+
return None if discard else value
|
|
383
|
+
|
|
384
|
+
def _read_binary_array(self):
|
|
385
|
+
object_id = self._i32()
|
|
386
|
+
array_type = self._u8()
|
|
387
|
+
rank = self._i32()
|
|
388
|
+
if rank < 1 or rank > 32:
|
|
389
|
+
raise self._error("invalid array rank %d" % rank)
|
|
390
|
+
lengths = [self._i32() for unused in range(rank)]
|
|
391
|
+
if any(length < 0 for length in lengths):
|
|
392
|
+
raise self._error("negative array length")
|
|
393
|
+
if array_type in _ARRAY_OFFSET_TYPES:
|
|
394
|
+
for unused in range(rank):
|
|
395
|
+
self._i32()
|
|
396
|
+
binary_type = self._u8()
|
|
397
|
+
additional = None
|
|
398
|
+
if binary_type in (_TYPE_PRIMITIVE, _TYPE_PRIMITIVE_ARRAY):
|
|
399
|
+
additional = self._u8()
|
|
400
|
+
elif binary_type == _TYPE_SYSTEM_CLASS:
|
|
401
|
+
additional = self._string()
|
|
402
|
+
elif binary_type == _TYPE_CLASS:
|
|
403
|
+
additional = (self._string(), self._i32())
|
|
404
|
+
elif binary_type not in (1, 2, 5, 6):
|
|
405
|
+
raise self._error("unsupported array binary type %d" % binary_type)
|
|
406
|
+
|
|
407
|
+
length = 1
|
|
408
|
+
for dimension in lengths:
|
|
409
|
+
length *= dimension
|
|
410
|
+
if binary_type == _TYPE_PRIMITIVE:
|
|
411
|
+
value = self._primitive_values(length, additional)
|
|
412
|
+
else:
|
|
413
|
+
class_name = additional[0] if binary_type == _TYPE_CLASS else additional
|
|
414
|
+
value = self._read_array_values(
|
|
415
|
+
length, discard=class_name in _DISCARDED_ARRAY_CLASSES
|
|
416
|
+
)
|
|
417
|
+
self.objects[object_id] = value
|
|
418
|
+
return value
|