mne-tools / mne-tools/mne-python

SourceChGain and SourceChOffset not applied - BCI2k reader

Open
#13,851 8 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
3.5k
Forks
1.6k
Avg merge
1d 6h
Merged PRs (30d)
100

Description

Follow up Issue for Improving BCI2k reader, since #13699 is merged

What the spec says (Required Parameters - Section Source):

Both SourceChGain and SourceChOffset are listed as required floatlist parameters in every BCI2000 data file:

SourceChOffset - raw data zero offset in AD units
SourceChGain - factor to convert raw AD units into µV

What a real file actually looks like (from a recorded .dat header):

Image

Two things to notice here:

  1. The first token after = is the list length, not a value. 2 means "there are 2 channels, and 2 values follow". The actual per-channel values start from the second token onward.

  2. The unit string (muV) is embedded inside each value token — e.g. 0.1muV is not a plain float. It must be split into a numeric part (0.1) and a unit part (muV) before any arithmetic.

What the current reader does:

value = right.strip().split()[0]   # grabs "2" — the list length, not a gain value
params[name] = value

This stores only the list-length count (2) and throws away all the actual per-channel values silently. Because of this, SourceChGain and SourceChOffset are never applied, and the signal is returned in raw ADC counts with no warning to the user.

What the fix needs to do :

Parse list parameters correctly:

After stripping the // comment, detect that the first token is an integer count n, then collect the next n tokens as the value list:

# strip inline comment first
rhs = right.split("//")[0].strip()
tokens = rhs.split()
# first token is list length for floatlist params
n = int(tokens[0])
raw_values = tokens[1 : n + 1]   # e.g. ["0.1muV", "0.1muV"]

Step 2 - Strip embedded unit strings and convert to volts:

The unit token (e.g. muV, mV, V) must be parsed out of each value before converting to a float. MNE stores EEG data internally in volts (V), so the final scaled value must be in V:

import re

_UNIT_SCALE = {"v": 1.0, "mv": 1e-3, "muv": 1e-6, "uv": 1e-6, "nv": 1e-9}

def _parse_value_with_unit(token):
    """Split '0.1muV' into (0.1, 1e-6). Returns (float, scale_to_V)."""
    m = re.match(r"([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*([a-zA-Z]*)", token)
    num = float(m.group(1))
    unit = m.group(2).lower().rstrip("v") + "v" if m.group(2) else "v"
    scale = _UNIT_SCALE.get(unit, 1.0)
    return num, scale

Then apply offset and gain per channel before passing to BaseRaw:

for ch in range(n_channels):
    offset, _ = _parse_value_with_unit(offsets[ch])   # offset is dimensionless (AD units)
    gain, scale = _parse_value_with_unit(gains[ch])   # gain * scale => V per AD count
    signal[ch] = (signal[ch] + offset) * gain * scale  # now in V

cc @larsoner

References

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in the BCI2k reader at the parameter parsing code containing params[name] = value, then trace how those parameters reach BaseRaw. Use the generated test file referenced in the issue to verify list lengths, embedded units, and per-channel scaling; done means gain and offset are applied and returned data is in volts.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
data
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.