|
24 | 24 | ) |
25 | 25 | from neo.core import NeoReadWriteError |
26 | 26 |
|
| 27 | +# A decimal number followed by an optional unit |
| 28 | +# Example: "0.01617", "-1.5e-3", "0.003muV", "256.0Hz" |
| 29 | +# Format: {number}{unit} |
| 30 | +# Regex tokens: |
| 31 | +# [-+]? → optional sign |
| 32 | +# \d+\.?\d* → digits with an optional decimal part, e.g. "43", "5.", "0.01617" |
| 33 | +# \.\d+ → or a number that starts at the decimal point, e.g. ".5" |
| 34 | +# [eE][-+]?\d+ → optional exponent, e.g. "e-3" |
| 35 | +# \w* → the unit, e.g. "muV", possibly empty |
| 36 | +_number_pattern = r"(?P<number>[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)(?P<unit>\w*)" |
| 37 | + |
27 | 38 |
|
28 | 39 | class BCI2000RawIO(BaseRawIO): |
29 | 40 | """ |
@@ -74,16 +85,16 @@ def _parse_header(self): |
74 | 85 | gain = param_defs["SourceChGain"]["value"][chan_ix] |
75 | 86 |
|
76 | 87 | if isinstance(gain, str): |
77 | | - r = re.findall(r"(\d+)(\D+)", gain) |
78 | 88 | # some files have strange units attached to gain |
79 | 89 | # in that case it is ignored |
80 | | - if len(r) == 1: |
81 | | - gain = r[0][0] |
82 | | - gain = float(gain) |
| 90 | + gain = float(re.match(_number_pattern, gain).group("number")) |
83 | 91 |
|
84 | 92 | offset = param_defs["SourceChOffset"]["value"][chan_ix] |
85 | 93 | if isinstance(offset, str): |
86 | | - offset = float(offset) |
| 94 | + offset = float(re.match(_number_pattern, offset).group("number")) |
| 95 | + # BCI2000 defines uV = (raw - SourceChOffset) * SourceChGain |
| 96 | + # while neo computes raw * gain + offset |
| 97 | + offset = -offset * gain |
87 | 98 |
|
88 | 99 | sig_channels.append((ch_name, chan_id, sr, dtype, units, gain, offset, stream_id, buffer_id)) |
89 | 100 | self.header["signal_channels"] = np.array(sig_channels, dtype=_signal_channel_dtype) |
@@ -280,7 +291,8 @@ def rescale_value(param_val, data_type): |
280 | 291 | if param_val.lower().startswith("0x"): |
281 | 292 | param_val = int(param_val, 16) |
282 | 293 | elif data_type in ["int", "float"]: |
283 | | - matches = re.match(r"(-*\d+)(\w*)", param_val) |
| 294 | + pattern = r"(-*\d+)(\w*)" if data_type == "int" else _number_pattern |
| 295 | + matches = re.match(pattern, param_val) |
284 | 296 | if matches is not None: # Can be None for % in def, min, max vals |
285 | 297 | param_val, unit_str = matches.group(1), matches.group(2) |
286 | 298 | param_val = int(param_val) if data_type == "int" else float(param_val) |
@@ -325,7 +337,9 @@ def parse_dimensions(param_list): |
325 | 337 | if temp != "[ State Vector Definition ]": |
326 | 338 | raise NeoReadWriteError(f"State definitions not found in header {filename}") |
327 | 339 | state_defs = [] |
328 | | - state_def_dtype = [("name", "a64"), ("length", int), ("startVal", int), ("bytePos", int), ("bitPos", int)] |
| 340 | + # "S64" because NumPy 2.0 deprecated the "a64" alias (https://github.com/numpy/numpy/pull/24854) |
| 341 | + # and NumPy 2.5 removed it, see https://github.com/NeuralEnsemble/python-neo/pull/1932 |
| 342 | + state_def_dtype = [("name", "S64"), ("length", int), ("startVal", int), ("bytePos", int), ("bitPos", int)] |
329 | 343 | while True: |
330 | 344 | temp = fid.readline().decode("utf8").strip() |
331 | 345 | if len(temp) == 0 or temp[0] == "[": |
|
0 commit comments