Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions neo/rawio/bci2000rawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,16 @@ def _parse_header(self):
gain = param_defs["SourceChGain"]["value"][chan_ix]

if isinstance(gain, str):
r = re.findall(r"(\d+)(\D+)", gain)
# some files have strange units attached to gain
# in that case it is ignored
if len(r) == 1:
gain = r[0][0]
gain = float(gain)
gain = float(re.match(_number_pattern, gain).group(1))

offset = param_defs["SourceChOffset"]["value"][chan_ix]
if isinstance(offset, str):
offset = float(offset)
offset = float(re.match(_number_pattern, offset).group(1))
# BCI2000 defines uV = (raw - SourceChOffset) * SourceChGain
# while neo computes raw * gain + offset
offset = -offset * gain

sig_channels.append((ch_name, chan_id, sr, dtype, units, gain, offset, stream_id, buffer_id))
self.header["signal_channels"] = np.array(sig_channels, dtype=_signal_channel_dtype)
Expand Down Expand Up @@ -254,6 +254,10 @@ def _event_arrays_list(self):
return self._my_events


# a decimal number such as "0.003" or "-1.5e-3" followed by optional units such as "muV"
_number_pattern = r"([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)(\w*)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One tiny readability thing from my perspective. Could we move this to near the top of the file (like below imports) or near the bottom so it's easier to find in the future. Mid-file will just make it hard to find _number_pattern if I'm scanning the file without ctrl+f



def parse_bci2000_header(filename):
# typically we want parameter values in Hz, seconds, or microvolts.
scales_dict = {
Expand All @@ -280,7 +284,8 @@ def rescale_value(param_val, data_type):
if param_val.lower().startswith("0x"):
param_val = int(param_val, 16)
elif data_type in ["int", "float"]:
matches = re.match(r"(-*\d+)(\w*)", param_val)
pattern = r"(-*\d+)(\w*)" if data_type == "int" else _number_pattern
matches = re.match(pattern, param_val)
if matches is not None: # Can be None for % in def, min, max vals
param_val, unit_str = matches.group(1), matches.group(2)
param_val = int(param_val) if data_type == "int" else float(param_val)
Expand Down Expand Up @@ -325,7 +330,7 @@ def parse_dimensions(param_list):
if temp != "[ State Vector Definition ]":
raise NeoReadWriteError(f"State definitions not found in header {filename}")
state_defs = []
state_def_dtype = [("name", "a64"), ("length", int), ("startVal", int), ("bytePos", int), ("bitPos", int)]
state_def_dtype = [("name", "S64"), ("length", int), ("startVal", int), ("bytePos", int), ("bitPos", int)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also love a comment here just mentioning linking back to your PR mentioning the deprecation. If NumPy did it once they could deprecate these string arguments in the future and it is nice to have a quick reference to making these switches.

while True:
temp = fid.readline().decode("utf8").strip()
if len(temp) == 0 or temp[0] == "[":
Expand Down
21 changes: 19 additions & 2 deletions neo/test/rawiotest/test_bci2000rawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import unittest

import numpy as np

from neo.rawio.bci2000rawio import BCI2000RawIO
from neo.test.rawiotest.common_rawio_test import BaseTestRawIO

Expand All @@ -14,12 +16,27 @@ class TestBCI2000RawIO(
):
rawioclass = BCI2000RawIO

entities_to_download = ["bci2000/eeg1_1.dat", "bci2000/eeg1_2.dat", "bci2000/eeg1_3.dat"]

entities_to_download = [
"bci2000",
]

entities_to_test = ["bci2000/eeg1_1.dat", "bci2000/eeg1_2.dat", "bci2000/eeg1_3.dat"]

def test_scaling(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me think about this test for a moment in general too.

# The header has SourceChGain= 64 0.01617 0.01591 ... and SourceChOffset= 64 43 55 ...
# BCI2000 defines uV = (raw - SourceChOffset) * SourceChGain
reader = BCI2000RawIO(filename=self.get_local_path("bci2000/eeg1_1.dat"))
reader.parse_header()
signal_channels = reader.header["signal_channels"]
np.testing.assert_allclose(signal_channels["gain"][:2], [0.01617, 0.01591], rtol=1e-6)

raw = reader.get_analogsignal_chunk(0, 0, 0, 3, 0, channel_indexes=[0, 1])
uV = reader.rescale_signal_raw_to_float(raw, dtype="float64", stream_index=0, channel_indexes=[0, 1])
expected = (raw.astype("float64") - [43, 55]) * [0.01617, 0.01591]
np.testing.assert_allclose(uV, expected, rtol=1e-5)
# first samples as decoded by BCI2000's own FileReader.py (BCI2kReader)
np.testing.assert_allclose(uV[:, 0], [-16.218512, 1.3744501, -9.23307], rtol=1e-5)


if __name__ == "__main__":
unittest.main()