From 197649b1aca8208035ba04b0136dbf1b65fa70b7 Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Fri, 27 Feb 2026 01:59:27 +0530
Subject: [PATCH 01/26] ENH: Add initial BCI2000 .dat reader (preload-only)

---
 mne/io/__init__.pyi              |   2 +
 mne/io/bci2k/__init__.py         |   1 +
 mne/io/bci2k/bci2k.py            | 264 +++++++++++++++++++++++++++++++
 mne/io/bci2k/tests/__init__.py   |   0
 mne/io/bci2k/tests/test_bci2k.py |  90 +++++++++++
 5 files changed, 357 insertions(+)
 create mode 100644 mne/io/bci2k/__init__.py
 create mode 100644 mne/io/bci2k/bci2k.py
 create mode 100644 mne/io/bci2k/tests/__init__.py
 create mode 100644 mne/io/bci2k/tests/test_bci2k.py

diff --git a/mne/io/__init__.pyi b/mne/io/__init__.pyi
index d7cd5bdfe02..f017b863d4c 100644
--- a/mne/io/__init__.pyi
+++ b/mne/io/__init__.pyi
@@ -46,6 +46,7 @@ __all__ = [
     "read_raw_nsx",
     "read_raw_persyst",
     "read_raw_snirf",
+    "read_raw_bci2k",
     "show_fiff",
     "write_fiducials",
     "write_info",
@@ -91,3 +92,4 @@ from .nirx import read_raw_nirx
 from .nsx import read_raw_nsx
 from .persyst import read_raw_persyst
 from .snirf import read_raw_snirf
+from .bci2k import read_raw_bci2k
diff --git a/mne/io/bci2k/__init__.py b/mne/io/bci2k/__init__.py
new file mode 100644
index 00000000000..3ed9f705773
--- /dev/null
+++ b/mne/io/bci2k/__init__.py
@@ -0,0 +1 @@
+from .bci2k import read_raw_bci2k, RawBCI2k
\ No newline at end of file
diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
new file mode 100644
index 00000000000..3e5590b5049
--- /dev/null
+++ b/mne/io/bci2k/bci2k.py
@@ -0,0 +1,264 @@
+import os
+import re
+import numpy as np
+import mne
+
+from ..base import BaseRaw
+from ...annotations import Annotations
+from ...utils import verbose
+
+
+def _parse_bci2k_header(fname):
+    """Parse minimal BCI2000 .dat header.
+
+    This parser intended to extract:
+    - Header length (bytes)
+    - Number of source channels
+    - Statevector length (bytes)
+    - Data format (int16/int32/float32)
+    - Sampling rate
+    - State definitions (name, length, bytePos, bitPos)
+    """
+    header = {}
+    params = {}
+    state_defs = {}
+
+    def _parse_sampling_rate(val):
+        # Accept e.g. "256", "256Hz", "256.0 Hz"
+        text = str(val).strip()
+        text = re.sub(r"\s*Hz\s*$", "", text, flags=re.IGNORECASE)
+        # Grab the first float-looking token
+        m = re.search(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?", text)
+        if m is None:
+            raise ValueError(f"Could not parse SamplingRate from {val!r}")
+        return float(m.group(0))
+
+    current_section = ""
+
+    with open(fname, "rb") as f:
+        # First line: key=value pairs
+        first_line = f.readline().decode("utf-8", errors="replace").strip()
+        for token in first_line.split():
+            if "=" in token:
+                k, v = token.split("=", 1)
+                header[k] = v
+
+        missing = [k for k in ("HeaderLen", "SourceCh", "StatevectorLen") if k not in header]
+        if missing:
+            raise ValueError(
+                "BCI2000 header is missing required key(s): "
+                f"{', '.join(missing)}"
+            )
+
+        header_len = int(header["HeaderLen"])
+        n_channels = int(header["SourceCh"])
+        state_vec_len = int(header["StatevectorLen"])
+        data_format = header.get("DataFormat", "int16")
+
+        # Move through header sections up to header_len bytes
+        while f.tell() < header_len:
+            line = f.readline().decode("utf-8", errors="replace").strip()
+            if not line:
+                continue
+
+            if line.startswith("[") and line.endswith("]"):
+                current_section = line
+                continue
+
+            # Parameter section: try to parse "Name=Value"
+            if "Parameter Definition" in current_section:
+                if "=" in line:
+                    left, right = line.split("=", 1)
+                    name = left.strip().split()[-1]
+                    value = right.strip().split()[0]
+                    params[name] = value
+                continue
+
+            # State section: typically "Name Length Default BytePos BitPos"
+            if "State Vector Definition" in current_section:
+                parts = line.split()
+                if len(parts) >= 5:
+                    try:
+                        length = int(parts[1])
+                        byte_pos = int(parts[3])
+                        bit_pos = int(parts[4])
+                    except ValueError:
+                        # Skip malformed or binary-contaminated lines
+                        continue
+                    state_defs[parts[0]] = {
+                        "length": length,
+                        "bytePos": byte_pos,
+                        "bitPos": bit_pos,
+                    }
+                continue
+
+    if "SamplingRate" not in params:
+        raise ValueError(
+            "Could not find 'SamplingRate' in the BCI2000 Parameter Definition section."
+        )
+
+    sfreq = _parse_sampling_rate(params["SamplingRate"])
+
+    return {
+        "header_len": header_len,
+        "n_channels": n_channels,
+        "state_vec_len": state_vec_len,
+        "data_format": data_format,
+        "sfreq": sfreq,
+        "params": params,
+        "state_defs": state_defs,
+    }
+
+
+def _read_bci2k_data(fname, info_dict):
+    """Read binary signal + state data."""
+
+    header_len = info_dict["header_len"]
+    n_channels = info_dict["n_channels"]
+    state_vec_len = info_dict["state_vec_len"]
+    data_format = info_dict["data_format"]
+
+    # Determine dtype
+    if data_format == "int16":
+        dtype = np.int16
+    elif data_format == "int32":
+        dtype = np.int32
+    elif data_format == "float32":
+        dtype = np.float32
+    else:
+        raise ValueError(f"Unsupported DataFormat: {data_format}")
+
+    bytes_per_sample = np.dtype(dtype).itemsize
+    bytes_per_frame = n_channels * bytes_per_sample + state_vec_len
+
+    file_size = os.path.getsize(fname)
+    data_bytes = file_size - header_len
+    n_samples = data_bytes // bytes_per_frame
+
+    with open(fname, "rb") as f:
+        f.seek(header_len)
+        raw = f.read(n_samples * bytes_per_frame)
+
+    raw = np.frombuffer(raw, dtype=np.uint8)
+
+    # Separate signal + state
+    frame_data = raw.reshape(n_samples, bytes_per_frame)
+    sig_bytes = frame_data[:, : n_channels * bytes_per_sample]
+    state_bytes = frame_data[:, n_channels * bytes_per_sample :]
+
+    signal = np.frombuffer(sig_bytes.tobytes(), dtype=dtype).reshape(
+        n_samples, n_channels
+    )
+
+    signal = signal.T.astype(np.float64)  # (n_channels, n_samples)
+    state_bytes = state_bytes.T  # (state_vec_len, n_samples), dtype=uint8
+
+    return signal, state_bytes
+
+
+def _decode_bci2k_states(state_bytes, state_defs):
+    """Decode BCI2000 state vector into integer state time series.
+
+    Parameters
+    ----------
+    state_bytes : array, shape (n_bytes, n_samples), dtype=uint8
+        Raw state vector bytes for each sample.
+    state_defs : dict
+        Mapping state name -> dict(length, bytePos, bitPos).
+
+    Returns
+    -------
+    states : dict
+        Mapping state name -> array, shape (n_samples,), dtype=int32.
+    """
+    if state_bytes.size == 0 or not state_defs:
+        return {}
+
+    n_bytes, n_samples = state_bytes.shape
+    states = {}
+
+    for name, sdef in state_defs.items():
+        length = int(sdef["length"])
+        byte_pos = int(sdef["bytePos"])
+        bit_pos = int(sdef["bitPos"])
+
+        vals = np.zeros(n_samples, dtype=np.int32)
+
+        # Decode bit by bit according to spec
+        for bit in range(length):
+            offset = bit_pos + bit
+            this_byte = byte_pos + offset // 8
+            this_bit = offset % 8
+            if this_byte < 0 or this_byte >= n_bytes:
+                continue
+            mask = 1 << this_bit
+            bit_vals = (state_bytes[this_byte] & mask) >> this_bit
+            vals |= (bit_vals.astype(np.int32) << bit)
+
+        states[name] = vals
+
+    return states
+
+
+class RawBCI2k(BaseRaw):
+
+    @verbose
+    def __init__(self, input_fname, preload=False, verbose=None):
+        # For now we always preload; non-preload would require chunked reading.
+        if not preload:
+            raise NotImplementedError(
+                "preload=False is not yet supported for BCI2000; "
+                "use preload=True for now."
+            )
+
+        info_dict = _parse_bci2k_header(input_fname)
+        signal, state_bytes = _read_bci2k_data(input_fname, info_dict)
+
+        sfreq = info_dict["sfreq"]
+        n_channels = info_dict["n_channels"]
+
+        # Channel names: use generic EEG names for now; can be improved later
+        ch_names = [f"EEG{i + 1}" for i in range(n_channels)]
+        ch_types = ["eeg"] * n_channels
+
+        # Decode states into integer series
+        states = _decode_bci2k_states(state_bytes, info_dict["state_defs"])
+
+        stim_data = None
+        if "StimulusCode" in states:
+            stim_data = states["StimulusCode"].astype(np.float32)[np.newaxis, :]
+            ch_names.append("STI 014")
+            ch_types.append("stim")
+            signal = np.vstack([signal, stim_data])
+
+        info = mne.create_info(
+            ch_names=ch_names,
+            sfreq=sfreq,
+            ch_types=ch_types,
+        )
+
+        n_samp = signal.shape[1]
+        first_samp = 0
+        last_samp = n_samp - 1
+
+        # Proper BaseRaw initialization: pass preloaded data array directly
+        super().__init__(
+            info,
+            preload=signal,  # (n_channels, n_times) array
+            filenames=[input_fname],
+            first_samps=[first_samp],
+            last_samps=[last_samp],
+            orig_format="single",
+            verbose=verbose,
+        )
+
+        self._bci2k_states = states 
+
+
+def read_raw_bci2k(input_fname, preload=False, verbose=None):
+    """Reader for BCI2000 .dat files."""
+    return RawBCI2k(
+        input_fname=input_fname,
+        preload=preload,
+        verbose=verbose,
+    )
\ No newline at end of file
diff --git a/mne/io/bci2k/tests/__init__.py b/mne/io/bci2k/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/mne/io/bci2k/tests/test_bci2k.py b/mne/io/bci2k/tests/test_bci2k.py
new file mode 100644
index 00000000000..342107867ef
--- /dev/null
+++ b/mne/io/bci2k/tests/test_bci2k.py
@@ -0,0 +1,90 @@
+import os
+import struct
+
+import numpy as np
+import pytest
+
+from mne.io.bci2k import read_raw_bci2k, RawBCI2k
+
+def _write_demo_bci2k(fname, n_channels=2, n_samples=100, sfreq=256,
+                      state_vec_len=1, data_format="int16", header_len=1024):
+    """Write a minimal BCI2000 .dat demo file compatible with our reader."""
+    header_lines = []
+
+    # First line: key=value pairs
+    first_line = (
+        f"BCI2000V=3.0 "
+        f"HeaderLen={header_len} "
+        f"SourceCh={n_channels} "
+        f"StatevectorLen={state_vec_len} "
+        f"DataFormat={data_format}"
+    )
+    header_lines.append(first_line)
+
+    # Minimal Parameter Definition section
+    header_lines.append("[ Parameter Definition ]")
+    header_lines.append(f"SamplingRate= {sfreq}Hz")
+
+    # State Vector Definition section: StimulusCode 8 bits at byte 0, bit 0
+    header_lines.append("[ State Vector Definition ]")
+    header_lines.append("StimulusCode 8 0 0 0")
+
+    header_text = "\n".join(header_lines) + "\n"
+    header_bytes = header_text.encode("utf-8")
+    if len(header_bytes) > header_len:
+        raise RuntimeError("Header too long for requested HeaderLen")
+    header_bytes += b"\x00" * (header_len - len(header_bytes))
+
+    # Signal data on disk
+    rng = np.random.default_rng(42)
+    if data_format != "int16":
+        raise RuntimeError("Demo writer currently assumes int16 data_format")
+    signal = rng.integers(-100, 100, size=(n_samples, n_channels),
+                          dtype=np.int16)
+
+    # StimulusCode: increases every 10 samples
+    state = np.zeros((n_samples,), dtype=np.uint8)
+    for i in range(n_samples):
+        state[i] = i // 10
+
+    frames = []
+    for i in range(n_samples):
+        frames.append(signal[i].tobytes() + struct.pack("B", state[i]))
+    data_bytes = b"".join(frames)
+
+    os.makedirs(os.path.dirname(fname), exist_ok=True)
+    with open(fname, "wb") as f:
+        f.write(header_bytes)
+        f.write(data_bytes)
+
+
+def test_read_bci2k_demo_file(tmp_path):
+    """Basic smoke test for BCI2000 .dat reader."""
+    fname = tmp_path / "demo_bci2k.dat"
+    _write_demo_bci2k(str(fname))
+
+    raw = read_raw_bci2k(str(fname), preload=True)
+
+    assert isinstance(raw, RawBCI2k)
+    assert raw.info["sfreq"] == 256.0
+    assert raw.n_times == 100
+
+    # 2 EEG + 1 stim
+    assert raw.ch_names == ["EEG1", "EEG2", "STI 014"]
+
+    data = raw.get_data()
+    assert data.shape == (3, 100)
+
+    # Check StimulusCode decoding (0..9, each repeated 10 times)
+    stim = raw._bci2k_states["StimulusCode"]
+    expected = np.repeat(np.arange(10, dtype=np.int32), 10)
+    np.testing.assert_array_equal(stim, expected)
+
+
+def test_bci2k_preload_false_raises(tmp_path):
+    """preload=False is not yet supported."""
+    fname = tmp_path / "demo_bci2k.dat"
+    _write_demo_bci2k(str(fname))
+
+    with pytest.raises(NotImplementedError, match="preload=False"):
+        read_raw_bci2k(str(fname), preload=False)
\ No newline at end of file

From 2c83c2a01f0f01a7fda0c6b8d3d6cfa39c2cb83b Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 26 Feb 2026 20:56:35 +0000
Subject: [PATCH 02/26] [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
---
 mne/io/__init__.pyi              |  2 +-
 mne/io/bci2k/bci2k.py            | 20 ++++++++++----------
 mne/io/bci2k/tests/test_bci2k.py | 21 ++++++++++++++-------
 3 files changed, 25 insertions(+), 18 deletions(-)

diff --git a/mne/io/__init__.pyi b/mne/io/__init__.pyi
index f017b863d4c..f7b8e7a9450 100644
--- a/mne/io/__init__.pyi
+++ b/mne/io/__init__.pyi
@@ -66,6 +66,7 @@ from .ant import read_raw_ant
 from .array import RawArray
 from .artemis123 import read_raw_artemis123
 from .base import BaseRaw, concatenate_raws, match_channel_orders
+from .bci2k import read_raw_bci2k
 from .besa import read_evoked_besa
 from .boxy import read_raw_boxy
 from .brainvision import read_raw_brainvision
@@ -92,4 +93,3 @@ from .nirx import read_raw_nirx
 from .nsx import read_raw_nsx
 from .persyst import read_raw_persyst
 from .snirf import read_raw_snirf
-from .bci2k import read_raw_bci2k
diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index 3e5590b5049..7c7f315ccd2 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -1,11 +1,12 @@
 import os
 import re
+
 import numpy as np
+
 import mne
 
-from ..base import BaseRaw
-from ...annotations import Annotations
 from ...utils import verbose
+from ..base import BaseRaw
 
 
 def _parse_bci2k_header(fname):
@@ -43,11 +44,12 @@ def _parse_sampling_rate(val):
                 k, v = token.split("=", 1)
                 header[k] = v
 
-        missing = [k for k in ("HeaderLen", "SourceCh", "StatevectorLen") if k not in header]
+        missing = [
+            k for k in ("HeaderLen", "SourceCh", "StatevectorLen") if k not in header
+        ]
         if missing:
             raise ValueError(
-                "BCI2000 header is missing required key(s): "
-                f"{', '.join(missing)}"
+                f"BCI2000 header is missing required key(s): {', '.join(missing)}"
             )
 
         header_len = int(header["HeaderLen"])
@@ -112,7 +114,6 @@ def _parse_sampling_rate(val):
 
 def _read_bci2k_data(fname, info_dict):
     """Read binary signal + state data."""
-
     header_len = info_dict["header_len"]
     n_channels = info_dict["n_channels"]
     state_vec_len = info_dict["state_vec_len"]
@@ -193,7 +194,7 @@ def _decode_bci2k_states(state_bytes, state_defs):
                 continue
             mask = 1 << this_bit
             bit_vals = (state_bytes[this_byte] & mask) >> this_bit
-            vals |= (bit_vals.astype(np.int32) << bit)
+            vals |= bit_vals.astype(np.int32) << bit
 
         states[name] = vals
 
@@ -201,7 +202,6 @@ def _decode_bci2k_states(state_bytes, state_defs):
 
 
 class RawBCI2k(BaseRaw):
-
     @verbose
     def __init__(self, input_fname, preload=False, verbose=None):
         # For now we always preload; non-preload would require chunked reading.
@@ -252,7 +252,7 @@ def __init__(self, input_fname, preload=False, verbose=None):
             verbose=verbose,
         )
 
-        self._bci2k_states = states 
+        self._bci2k_states = states
 
 
 def read_raw_bci2k(input_fname, preload=False, verbose=None):
@@ -261,4 +261,4 @@ def read_raw_bci2k(input_fname, preload=False, verbose=None):
         input_fname=input_fname,
         preload=preload,
         verbose=verbose,
-    )
\ No newline at end of file
+    )
diff --git a/mne/io/bci2k/tests/test_bci2k.py b/mne/io/bci2k/tests/test_bci2k.py
index 342107867ef..4bc513637f1 100644
--- a/mne/io/bci2k/tests/test_bci2k.py
+++ b/mne/io/bci2k/tests/test_bci2k.py
@@ -4,10 +4,18 @@
 import numpy as np
 import pytest
 
-from mne.io.bci2k import read_raw_bci2k, RawBCI2k
-
-def _write_demo_bci2k(fname, n_channels=2, n_samples=100, sfreq=256,
-                      state_vec_len=1, data_format="int16", header_len=1024):
+from mne.io.bci2k import RawBCI2k, read_raw_bci2k
+
+
+def _write_demo_bci2k(
+    fname,
+    n_channels=2,
+    n_samples=100,
+    sfreq=256,
+    state_vec_len=1,
+    data_format="int16",
+    header_len=1024,
+):
     """Write a minimal BCI2000 .dat demo file compatible with our reader."""
     header_lines = []
 
@@ -39,8 +47,7 @@ def _write_demo_bci2k(fname, n_channels=2, n_samples=100, sfreq=256,
     rng = np.random.default_rng(42)
     if data_format != "int16":
         raise RuntimeError("Demo writer currently assumes int16 data_format")
-    signal = rng.integers(-100, 100, size=(n_samples, n_channels),
-                          dtype=np.int16)
+    signal = rng.integers(-100, 100, size=(n_samples, n_channels), dtype=np.int16)
 
     # StimulusCode: increases every 10 samples
     state = np.zeros((n_samples,), dtype=np.uint8)
@@ -87,4 +94,4 @@ def test_bci2k_preload_false_raises(tmp_path):
     _write_demo_bci2k(str(fname))
 
     with pytest.raises(NotImplementedError, match="preload=False"):
-        read_raw_bci2k(str(fname), preload=False)
\ No newline at end of file
+        read_raw_bci2k(str(fname), preload=False)

From 20a89f570becd0aa0212b488a05b3d43b8397851 Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Fri, 27 Feb 2026 03:02:08 +0530
Subject: [PATCH 03/26] MAINT: Address CI feedback (style and import fixes)

---
 mne/io/bci2k/bci2k.py | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index 7c7f315ccd2..fd3fd4fa14d 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -3,8 +3,7 @@
 
 import numpy as np
 
-import mne
-
+from ... import create_info
 from ...utils import verbose
 from ..base import BaseRaw
 
@@ -45,7 +44,10 @@ def _parse_sampling_rate(val):
                 header[k] = v
 
         missing = [
-            k for k in ("HeaderLen", "SourceCh", "StatevectorLen") if k not in header
+
+            k
+            for k in ("HeaderLen", "SourceCh", "StatevectorLen")
+            if k not in header
         ]
         if missing:
             raise ValueError(
@@ -202,6 +204,18 @@ def _decode_bci2k_states(state_bytes, state_defs):
 
 
 class RawBCI2k(BaseRaw):
+    """Raw object for BCI2000 .dat files.
+
+    Parameters
+    ----------
+    input_fname : path-like
+        Path to the BCI2000 .dat file.
+    preload : bool
+        Must be True. preload=False is not supported.
+    verbose : bool | str | int | None
+        Control verbosity.
+    """
+
     @verbose
     def __init__(self, input_fname, preload=False, verbose=None):
         # For now we always preload; non-preload would require chunked reading.
@@ -231,7 +245,7 @@ def __init__(self, input_fname, preload=False, verbose=None):
             ch_types.append("stim")
             signal = np.vstack([signal, stim_data])
 
-        info = mne.create_info(
+        info = create_info(
             ch_names=ch_names,
             sfreq=sfreq,
             ch_types=ch_types,

From 31bb511a5f1b90b9e33662257ea2799669b0dda5 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 26 Feb 2026 21:42:07 +0000
Subject: [PATCH 04/26] [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
---
 mne/io/bci2k/bci2k.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index fd3fd4fa14d..f35a76500c7 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -44,10 +44,7 @@ def _parse_sampling_rate(val):
                 header[k] = v
 
         missing = [
-
-            k
-            for k in ("HeaderLen", "SourceCh", "StatevectorLen")
-            if k not in header
+            k for k in ("HeaderLen", "SourceCh", "StatevectorLen") if k not in header
         ]
         if missing:
             raise ValueError(

From 21d9519cb411bfffd584a65858abef3dc4e5f38a Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Fri, 27 Feb 2026 20:38:04 +0530
Subject: [PATCH 05/26] MAINT: Use explicit relative import for create_info

---
 mne/io/bci2k/bci2k.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index f35a76500c7..af3bc66b1d2 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -3,7 +3,7 @@
 
 import numpy as np
 
-from ... import create_info
+from ..._fiff.meas_info import create_info
 from ...utils import verbose
 from ..base import BaseRaw
 

From fdca55934907848b34652279df074b84dfac0095 Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Fri, 27 Feb 2026 22:05:35 +0530
Subject: [PATCH 06/26] DOC: Add read_raw_bci2k to reading_raw_data autosummary

---
 doc/api/reading_raw_data.rst | 1 +
 1 file changed, 1 insertion(+)

diff --git a/doc/api/reading_raw_data.rst b/doc/api/reading_raw_data.rst
index 1dcf926792f..010f72f4d89 100644
--- a/doc/api/reading_raw_data.rst
+++ b/doc/api/reading_raw_data.rst
@@ -16,6 +16,7 @@ Reading raw data
    read_raw
    read_raw_ant
    read_raw_artemis123
+   read_raw_bci2k
    read_raw_bdf
    read_raw_boxy
    read_raw_brainvision

From 77dc37f2b84eb65edf440c70ff1997daf3f889ff Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Fri, 27 Feb 2026 22:49:03 +0530
Subject: [PATCH 07/26] DOC: Fix numpydoc format for read_raw_bci2k

---
 mne/io/bci2k/bci2k.py | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index af3bc66b1d2..21be58c3dc6 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -267,7 +267,22 @@ def __init__(self, input_fname, preload=False, verbose=None):
 
 
 def read_raw_bci2k(input_fname, preload=False, verbose=None):
-    """Reader for BCI2000 .dat files."""
+    """Read a BCI2000 ``.dat`` file.
+
+    Parameters
+    ----------
+    input_fname : path-like
+        Path to the BCI2000 ``.dat`` file.
+    preload : bool
+        If True, preload the data into memory. Must be True.
+    verbose : bool | str | int | None
+        Control verbosity.
+
+    Returns
+    -------
+    raw : instance of RawBCI2k
+        The raw object containing the data.
+    """
     return RawBCI2k(
         input_fname=input_fname,
         preload=preload,

From d871e263fa8e9468b359ce90da298856b09c2238 Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Tue, 3 Mar 2026 04:22:39 +0530
Subject: [PATCH 08/26] DOC/MAINT: Document read_raw_bci2k and fix
 numpydoc/sphinx warnings

---
 doc/conf.py           |  1 +
 mne/io/__init__.pyi   |  1 +
 mne/io/bci2k/bci2k.py | 11 ++++++++---
 3 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/doc/conf.py b/doc/conf.py
index 0d9fe79346e..e922d1117d3 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -289,6 +289,7 @@
     "EpochsEEGLAB": "mne.Epochs",
     "EpochsKIT": "mne.Epochs",
     "RawANT": "mne.io.Raw",
+    "RawBCI2k": "mne.io.Raw",
     "RawBOXY": "mne.io.Raw",
     "RawBrainVision": "mne.io.Raw",
     "RawBTi": "mne.io.Raw",
diff --git a/mne/io/__init__.pyi b/mne/io/__init__.pyi
index f7b8e7a9450..b23144d689f 100644
--- a/mne/io/__init__.pyi
+++ b/mne/io/__init__.pyi
@@ -19,6 +19,7 @@ __all__ = [
     "read_raw",
     "read_raw_ant",
     "read_raw_artemis123",
+    "read_raw_bci2k",
     "read_raw_bdf",
     "read_raw_boxy",
     "read_raw_brainvision",
diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index 21be58c3dc6..252c41a3a7e 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -11,7 +11,7 @@
 def _parse_bci2k_header(fname):
     """Parse minimal BCI2000 .dat header.
 
-    This parser intended to extract:
+    This parser is intended to extract:
     - Header length (bytes)
     - Number of source channels
     - Statevector length (bytes)
@@ -259,7 +259,7 @@ def __init__(self, input_fname, preload=False, verbose=None):
             filenames=[input_fname],
             first_samps=[first_samp],
             last_samps=[last_samp],
-            orig_format="single",
+            orig_format="double",
             verbose=verbose,
         )
 
@@ -274,7 +274,7 @@ def read_raw_bci2k(input_fname, preload=False, verbose=None):
     input_fname : path-like
         Path to the BCI2000 ``.dat`` file.
     preload : bool
-        If True, preload the data into memory. Must be True.
+        Whether to preload the data into memory. Must be True.
     verbose : bool | str | int | None
         Control verbosity.
 
@@ -282,6 +282,11 @@ def read_raw_bci2k(input_fname, preload=False, verbose=None):
     -------
     raw : instance of RawBCI2k
         The raw object containing the data.
+        See :class:`mne.io.Raw` for documentation of attributes and methods.
+
+    See Also
+    --------
+    mne.io.Raw : Documentation of attributes and methods of RawBCI2k.
     """
     return RawBCI2k(
         input_fname=input_fname,

From 090d2088a23dfe2e32b975d907a076ea47301331 Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Tue, 3 Mar 2026 16:50:25 +0530
Subject: [PATCH 09/26] DOC: Remove duplicate read_raw_bci2k from
 mne/io/tests/__init__.pyi

---
 mne/io/__init__.pyi | 1 -
 1 file changed, 1 deletion(-)

diff --git a/mne/io/__init__.pyi b/mne/io/__init__.pyi
index b23144d689f..11b86623b8e 100644
--- a/mne/io/__init__.pyi
+++ b/mne/io/__init__.pyi
@@ -47,7 +47,6 @@ __all__ = [
     "read_raw_nsx",
     "read_raw_persyst",
     "read_raw_snirf",
-    "read_raw_bci2k",
     "show_fiff",
     "write_fiducials",
     "write_info",

From 142967ed54ffad0291ffcae451edcdbd6754a3ae Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Sun, 15 Mar 2026 11:21:19 +0530
Subject: [PATCH 10/26] FIX: Improve BCI2000 header parsing for multi-field
 header line

---
 mne/io/bci2k/bci2k.py | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index 252c41a3a7e..cd5e9e8056b 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -36,12 +36,10 @@ def _parse_sampling_rate(val):
     current_section = ""
 
     with open(fname, "rb") as f:
-        # First line: key=value pairs
         first_line = f.readline().decode("utf-8", errors="replace").strip()
-        for token in first_line.split():
-            if "=" in token:
-                k, v = token.split("=", 1)
-                header[k] = v
+        pairs = re.findall(r'(\w+)\s*=\s*([^\s]+)', first_line)
+        for k, v in pairs:
+            header[k] = v
 
         missing = [
             k for k in ("HeaderLen", "SourceCh", "StatevectorLen") if k not in header

From 37f00ff7ae58edeeccd905eff4c5d77d3935f869 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Sun, 15 Mar 2026 05:53:25 +0000
Subject: [PATCH 11/26] [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
---
 mne/io/bci2k/bci2k.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index cd5e9e8056b..8bbbc78ddfc 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -37,7 +37,7 @@ def _parse_sampling_rate(val):
 
     with open(fname, "rb") as f:
         first_line = f.readline().decode("utf-8", errors="replace").strip()
-        pairs = re.findall(r'(\w+)\s*=\s*([^\s]+)', first_line)
+        pairs = re.findall(r"(\w+)\s*=\s*([^\s]+)", first_line)
         for k, v in pairs:
             header[k] = v
 

From 6f4033f6f1d0727e59af68f957917cd527bcbd9f Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Wed, 18 Mar 2026 02:55:22 +0530
Subject: [PATCH 12/26] TEST: use BCI2k test dataset from mne-testing-data in
 BCI2K reader tests

---
 mne/io/bci2k/bci2k.py            |   4 ++
 mne/io/bci2k/tests/test_bci2k.py | 106 +++++++------------------------
 2 files changed, 28 insertions(+), 82 deletions(-)

diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index cd5e9e8056b..5ca7a66ffd6 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -1,3 +1,7 @@
+# Authors: The MNE-Python contributors.
+# License: BSD-3-Clause
+# Copyright the MNE-Python contributors.
+
 import os
 import re
 
diff --git a/mne/io/bci2k/tests/test_bci2k.py b/mne/io/bci2k/tests/test_bci2k.py
index 4bc513637f1..98cfc49ec70 100644
--- a/mne/io/bci2k/tests/test_bci2k.py
+++ b/mne/io/bci2k/tests/test_bci2k.py
@@ -1,97 +1,39 @@
-import os
-import struct
+# Authors: The MNE-Python contributors.
+# License: BSD-3-Clause
+# Copyright the MNE-Python contributors.
 
-import numpy as np
-import pytest
-
-from mne.io.bci2k import RawBCI2k, read_raw_bci2k
-
-
-def _write_demo_bci2k(
-    fname,
-    n_channels=2,
-    n_samples=100,
-    sfreq=256,
-    state_vec_len=1,
-    data_format="int16",
-    header_len=1024,
-):
-    """Write a minimal BCI2000 .dat demo file compatible with our reader."""
-    header_lines = []
-
-    # First line: key=value pairs
-    first_line = (
-        f"BCI2000V=3.0 "
-        f"HeaderLen={header_len} "
-        f"SourceCh={n_channels} "
-        f"StatevectorLen={state_vec_len} "
-        f"DataFormat={data_format}"
-    )
-    header_lines.append(first_line)
-
-    # Minimal Parameter Definition section
-    header_lines.append("[ Parameter Definition ]")
-    header_lines.append(f"SamplingRate= {sfreq}Hz")
+from pathlib import Path
 
-    # State Vector Definition section: StimulusCode 8 bits at byte 0, bit 0
-    header_lines.append("[ State Vector Definition ]")
-    header_lines.append("StimulusCode 8 0 0 0")
-
-    header_text = "\n".join(header_lines) + "\n"
-    header_bytes = header_text.encode("utf-8")
-    if len(header_bytes) > header_len:
-        raise RuntimeError("Header too long for requested HeaderLen")
-    header_bytes += b"\x00" * (header_len - len(header_bytes))
-
-    # Signal data on disk
-    rng = np.random.default_rng(42)
-    if data_format != "int16":
-        raise RuntimeError("Demo writer currently assumes int16 data_format")
-    signal = rng.integers(-100, 100, size=(n_samples, n_channels), dtype=np.int16)
-
-    # StimulusCode: increases every 10 samples
-    state = np.zeros((n_samples,), dtype=np.uint8)
-    for i in range(n_samples):
-        state[i] = i // 10
+import pytest
 
-    frames = []
-    for i in range(n_samples):
-        frames.append(signal[i].tobytes() + struct.pack("B", state[i]))
-    data_bytes = b"".join(frames)
+import mne
+from mne.datasets import testing
 
-    os.makedirs(os.path.dirname(fname), exist_ok=True)
-    with open(fname, "wb") as f:
-        f.write(header_bytes)
-        f.write(data_bytes)
 
+data_path = testing.data_path(download=False)
+bci2k_fname = data_path / "BCI2k" / "bci2k_test.dat"
 
-def test_read_bci2k_demo_file(tmp_path):
-    """Basic smoke test for BCI2000 .dat reader."""
-    fname = tmp_path / "demo_bci2k.dat"
-    _write_demo_bci2k(str(fname))
 
-    raw = read_raw_bci2k(str(fname), preload=True)
+@testing.requires_testing_data
+def test_read_raw_bci2k():
+    """Test reading BCI2000 .dat file."""
+    raw = mne.io.read_raw_bci2k(bci2k_fname, preload=True)
 
-    assert isinstance(raw, RawBCI2k)
-    assert raw.info["sfreq"] == 256.0
-    assert raw.n_times == 100
+    assert raw.info["sfreq"] == 256
+    assert raw.info["nchan"] == 3
 
-    # 2 EEG + 1 stim
     assert raw.ch_names == ["EEG1", "EEG2", "STI 014"]
 
-    data = raw.get_data()
-    assert data.shape == (3, 100)
+    ch_types = raw.get_channel_types()
+    assert ch_types == ["eeg", "eeg", "stim"]
 
-    # Check StimulusCode decoding (0..9, each repeated 10 times)
-    stim = raw._bci2k_states["StimulusCode"]
-    expected = np.repeat(np.arange(10, dtype=np.int32), 10)
-    np.testing.assert_array_equal(stim, expected)
+    data = raw.get_data()
+    assert raw.get_data().shape == (3, raw.n_times)
 
+    assert raw.n_times > 0
 
-def test_bci2k_preload_false_raises(tmp_path):
-    """preload=False is not yet supported."""
-    fname = tmp_path / "demo_bci2k.dat"
-    _write_demo_bci2k(str(fname))
+    events = mne.find_events(raw, shortest_event=1)
 
-    with pytest.raises(NotImplementedError, match="preload=False"):
-        read_raw_bci2k(str(fname), preload=False)
+    assert events.ndim == 2
+    assert events.shape[1] == 3
+    assert "RawBCI2k" in repr(raw)
\ No newline at end of file

From 4073dff3eee0cd5723fab00bf3463e8f0ca62cbb Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 21:28:54 +0000
Subject: [PATCH 13/26] [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
---
 mne/io/bci2k/tests/test_bci2k.py | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/mne/io/bci2k/tests/test_bci2k.py b/mne/io/bci2k/tests/test_bci2k.py
index 98cfc49ec70..46681e5effb 100644
--- a/mne/io/bci2k/tests/test_bci2k.py
+++ b/mne/io/bci2k/tests/test_bci2k.py
@@ -2,14 +2,10 @@
 # License: BSD-3-Clause
 # Copyright the MNE-Python contributors.
 
-from pathlib import Path
-
-import pytest
 
 import mne
 from mne.datasets import testing
 
-
 data_path = testing.data_path(download=False)
 bci2k_fname = data_path / "BCI2k" / "bci2k_test.dat"
 
@@ -36,4 +32,4 @@ def test_read_raw_bci2k():
 
     assert events.ndim == 2
     assert events.shape[1] == 3
-    assert "RawBCI2k" in repr(raw)
\ No newline at end of file
+    assert "RawBCI2k" in repr(raw)

From dc50c118b8b622a2f8567c77db251ef785f74a82 Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Wed, 18 Mar 2026 03:08:27 +0530
Subject: [PATCH 14/26] TEST : remove unused imports and variables in BCI2k
 reader test

---
 mne/io/bci2k/tests/test_bci2k.py | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/mne/io/bci2k/tests/test_bci2k.py b/mne/io/bci2k/tests/test_bci2k.py
index 98cfc49ec70..b8324b1962b 100644
--- a/mne/io/bci2k/tests/test_bci2k.py
+++ b/mne/io/bci2k/tests/test_bci2k.py
@@ -2,10 +2,6 @@
 # License: BSD-3-Clause
 # Copyright the MNE-Python contributors.
 
-from pathlib import Path
-
-import pytest
-
 import mne
 from mne.datasets import testing
 
@@ -27,7 +23,6 @@ def test_read_raw_bci2k():
     ch_types = raw.get_channel_types()
     assert ch_types == ["eeg", "eeg", "stim"]
 
-    data = raw.get_data()
     assert raw.get_data().shape == (3, raw.n_times)
 
     assert raw.n_times > 0

From 3c763ad44452c32dda4b773620638eff13ab39dd Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 21:44:23 +0000
Subject: [PATCH 15/26] [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
---
 mne/io/bci2k/tests/test_bci2k.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mne/io/bci2k/tests/test_bci2k.py b/mne/io/bci2k/tests/test_bci2k.py
index 4cd04b946ec..3aad8661740 100644
--- a/mne/io/bci2k/tests/test_bci2k.py
+++ b/mne/io/bci2k/tests/test_bci2k.py
@@ -24,4 +24,4 @@ def test_read_raw_bci2k():
     events = mne.find_events(raw, shortest_event=1)
     assert events.ndim == 2
     assert events.shape[1] == 3
-    assert "RawBCI2k" in repr(raw)
\ No newline at end of file
+    assert "RawBCI2k" in repr(raw)

From 02c6d88abfd52ba98bb46815978e933ff76f42fe Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Thu, 19 Mar 2026 01:52:32 +0530
Subject: [PATCH 16/26] FIX: remove unused stim_data and _bci2k_states
 assignments

---
 mne/io/bci2k/bci2k.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index 3f1995cdb5b..62476a779d7 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -237,9 +237,8 @@ def __init__(self, input_fname, preload=False, verbose=None):
         # Decode states into integer series
         states = _decode_bci2k_states(state_bytes, info_dict["state_defs"])
 
-        stim_data = None
         if "StimulusCode" in states:
-            stim_data = states["StimulusCode"].astype(np.float32)[np.newaxis, :]
+            stim_data = states["StimulusCode"].astype(np.float64)[np.newaxis, :]
             ch_names.append("STI 014")
             ch_types.append("stim")
             signal = np.vstack([signal, stim_data])
@@ -265,8 +264,6 @@ def __init__(self, input_fname, preload=False, verbose=None):
             verbose=verbose,
         )
 
-        self._bci2k_states = states
-
 
 def read_raw_bci2k(input_fname, preload=False, verbose=None):
     """Read a BCI2000 ``.dat`` file.

From 346ae3874b8deb32bc6d3825b3c140d8d15ee3f0 Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Thu, 19 Mar 2026 04:33:47 +0530
Subject: [PATCH 17/26] ENH: add example for reading BCI2000 .dat files

---
 examples/io/read_bci2k.py | 63 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 63 insertions(+)
 create mode 100644 examples/io/read_bci2k.py

diff --git a/examples/io/read_bci2k.py b/examples/io/read_bci2k.py
new file mode 100644
index 00000000000..d9d423b9b2f
--- /dev/null
+++ b/examples/io/read_bci2k.py
@@ -0,0 +1,63 @@
+"""
+======================
+Reading BCI2000 files
+======================
+
+In this example, we use MNE-Python to read a BCI2000 ``.dat`` file.
+BCI2000 is a general-purpose brain-computer interface (BCI) system widely
+used in EEG research. The file is downloaded from the MNE testing data
+repository using :mod:`pooch`.
+
+"""  # noqa: D205 D400
+
+# Authors: The MNE-Python contributors.
+# License: BSD-3-Clause
+# Copyright the MNE-Python contributors.
+
+import pooch
+
+import mne
+
+# %%
+# First, we download the sample BCI2000 ``.dat`` file using :mod:`pooch`.
+
+fname = pooch.retrieve(
+    url="https://raw.githubusercontent.com/mne-tools/mne-testing-data/master/BCI2k/bci2k_test.dat",
+    known_hash="sha256:8efc7b5f700660a044086cb1449806ca408c2e6d32d9338c32e1bf31ce3ca9cb",
+)
+
+# %%
+# Now we can read the file using :func:`mne.io.read_raw_bci2k`.
+# Note that ``preload=True`` is required for BCI2000 files.
+ 
+raw = mne.io.read_raw_bci2k(fname, preload=True)
+print(raw.info)
+ 
+# %%
+# We can inspect the object representation, channel names, types, sampling
+# frequency, and recording duration.
+ 
+print(raw)
+print(f"Channel names : {raw.ch_names}")
+print(f"Channel types : {raw.get_channel_types()}")
+print(f"Sampling freq : {raw.info['sfreq']} Hz")
+print(f"Duration      : {raw.times[-1]:.2f} s")
+print(f"n_channels    : {raw.info['nchan']}")
+print(f"Data shape    : {raw.get_data().shape}  (n_channels, n_samples)")
+ 
+# %%
+# If the BCI2000 file contains a ``StimulusCode`` state, it is automatically
+# mapped to a ``STI 014`` stim channel. We can extract events from it using
+# :func:`mne.find_events`.
+ 
+if "STI 014" in raw.ch_names:
+    events = mne.find_events(raw, shortest_event=1)
+    print(f"Found {len(events)} events")
+    print(mne.count_events(events))
+else:
+    print("No stim channel found in this file.")
+ 
+# %%
+# Finally, we can visualize the raw data.
+ 
+raw.plot(duration=5, n_channels=len(raw.ch_names), scalings="auto")

From 8b1757e9894bc69f9fec4ddbc46d9bb9e00bf00c Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Wed, 18 Mar 2026 23:04:19 +0000
Subject: [PATCH 18/26] [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
---
 examples/io/read_bci2k.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/examples/io/read_bci2k.py b/examples/io/read_bci2k.py
index d9d423b9b2f..b2ac884011e 100644
--- a/examples/io/read_bci2k.py
+++ b/examples/io/read_bci2k.py
@@ -29,14 +29,14 @@
 # %%
 # Now we can read the file using :func:`mne.io.read_raw_bci2k`.
 # Note that ``preload=True`` is required for BCI2000 files.
- 
+
 raw = mne.io.read_raw_bci2k(fname, preload=True)
 print(raw.info)
- 
+
 # %%
 # We can inspect the object representation, channel names, types, sampling
 # frequency, and recording duration.
- 
+
 print(raw)
 print(f"Channel names : {raw.ch_names}")
 print(f"Channel types : {raw.get_channel_types()}")
@@ -44,20 +44,20 @@
 print(f"Duration      : {raw.times[-1]:.2f} s")
 print(f"n_channels    : {raw.info['nchan']}")
 print(f"Data shape    : {raw.get_data().shape}  (n_channels, n_samples)")
- 
+
 # %%
 # If the BCI2000 file contains a ``StimulusCode`` state, it is automatically
 # mapped to a ``STI 014`` stim channel. We can extract events from it using
 # :func:`mne.find_events`.
- 
+
 if "STI 014" in raw.ch_names:
     events = mne.find_events(raw, shortest_event=1)
     print(f"Found {len(events)} events")
     print(mne.count_events(events))
 else:
     print("No stim channel found in this file.")
- 
+
 # %%
 # Finally, we can visualize the raw data.
- 
+
 raw.plot(duration=5, n_channels=len(raw.ch_names), scalings="auto")

From 3b1688268f5d88c725bb7edc8810d19c96ed4304 Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Wed, 25 Mar 2026 02:16:47 +0530
Subject: [PATCH 19/26] FIX: move nested function outside , add bci2k_data save
 path in example/io/read_bci2k.py file

---
 examples/io/read_bci2k.py |  4 ++++
 mne/io/bci2k/bci2k.py     | 21 +++++++++++----------
 2 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/examples/io/read_bci2k.py b/examples/io/read_bci2k.py
index b2ac884011e..18b3c3b7d7f 100644
--- a/examples/io/read_bci2k.py
+++ b/examples/io/read_bci2k.py
@@ -21,9 +21,13 @@
 # %%
 # First, we download the sample BCI2000 ``.dat`` file using :mod:`pooch`.
 
+data_dir = mne.datasets.default_path() / "bci2k_data"
+data_dir.mkdir(exist_ok=True)
+
 fname = pooch.retrieve(
     url="https://raw.githubusercontent.com/mne-tools/mne-testing-data/master/BCI2k/bci2k_test.dat",
     known_hash="sha256:8efc7b5f700660a044086cb1449806ca408c2e6d32d9338c32e1bf31ce3ca9cb",
+    path=data_dir,
 )
 
 # %%
diff --git a/mne/io/bci2k/bci2k.py b/mne/io/bci2k/bci2k.py
index 62476a779d7..39cd709a11a 100644
--- a/mne/io/bci2k/bci2k.py
+++ b/mne/io/bci2k/bci2k.py
@@ -12,6 +12,17 @@
 from ..base import BaseRaw
 
 
+def _parse_sampling_rate(val):
+    # Accept e.g. "256", "256Hz", "256.0 Hz"
+    text = str(val).strip()
+    text = re.sub(r"\s*Hz\s*$", "", text, flags=re.IGNORECASE)
+    # Grab the first float-looking token
+    m = re.search(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?", text)
+    if m is None:
+        raise ValueError(f"Could not parse SamplingRate from {val!r}")
+    return float(m.group(0))
+
+
 def _parse_bci2k_header(fname):
     """Parse minimal BCI2000 .dat header.
 
@@ -27,16 +38,6 @@ def _parse_bci2k_header(fname):
     params = {}
     state_defs = {}
 
-    def _parse_sampling_rate(val):
-        # Accept e.g. "256", "256Hz", "256.0 Hz"
-        text = str(val).strip()
-        text = re.sub(r"\s*Hz\s*$", "", text, flags=re.IGNORECASE)
-        # Grab the first float-looking token
-        m = re.search(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?", text)
-        if m is None:
-            raise ValueError(f"Could not parse SamplingRate from {val!r}")
-        return float(m.group(0))
-
     current_section = ""
 
     with open(fname, "rb") as f:

From ab5c59702932d020019c0ff2b321060587ca3dc1 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Tue, 24 Mar 2026 20:47:59 +0000
Subject: [PATCH 20/26] [autofix.ci] apply automated fixes

---
 mne/io/bci2k/__init__.py       | 4 ++++
 mne/io/bci2k/tests/__init__.py | 3 +++
 2 files changed, 7 insertions(+)

diff --git a/mne/io/bci2k/__init__.py b/mne/io/bci2k/__init__.py
index 3ed9f705773..16e31647f9a 100644
--- a/mne/io/bci2k/__init__.py
+++ b/mne/io/bci2k/__init__.py
@@ -1 +1,5 @@
+# Authors: The MNE-Python contributors.
+# License: BSD-3-Clause
+# Copyright the MNE-Python contributors.
+
 from .bci2k import read_raw_bci2k, RawBCI2k
\ No newline at end of file
diff --git a/mne/io/bci2k/tests/__init__.py b/mne/io/bci2k/tests/__init__.py
index e69de29bb2d..04c673b4e4d 100644
--- a/mne/io/bci2k/tests/__init__.py
+++ b/mne/io/bci2k/tests/__init__.py
@@ -0,0 +1,3 @@
+# Authors: The MNE-Python contributors.
+# License: BSD-3-Clause
+# Copyright the MNE-Python contributors.

From c551e4d40c4feab4de6a59d0a2b2d57de2fca7ec Mon Sep 17 00:00:00 2001
From: Hansuja <hansujaigdtuwcseai@gmail.com>
Date: Wed, 25 Mar 2026 02:34:07 +0530
Subject: [PATCH 21/26] DOC : add changelog entry

---
 doc/changes/dev/13699.newfeature.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 doc/changes/dev/13699.newfeature.rst

diff --git a/doc/changes/dev/13699.newfeature.rst b/doc/changes/dev/13699.newfeature.rst
new file mode 100644
index 00000000000..0e16dbfa432
--- /dev/null
+++ b/doc/changes/dev/13699.newfeature.rst
@@ -0,0 +1 @@
+Add support for reading BCI2000 ``.dat`` files via :func:`mne.io.read_raw_bci2k`, and an example :file:`examples/io/read_bci2k.py` for downloading and visualizing BCI2000 data, by :newcontrib:`Hansuja Budhiraja`.
\ No newline at end of file

From 9c5dd223cbf8a085593d5c5a223e700e2f64a4c5 Mon Sep 17 00:00:00 2001
From: Eric Larson <larson.eric.d@gmail.com>
Date: Fri, 3 Apr 2026 17:20:51 -0400
Subject: [PATCH 22/26] FIX: Fixes

---
 examples/io/read_bci2k.py     | 2 +-
 mne/datasets/config.py        | 4 ++--
 mne/io/_read_raw.py           | 7 ++++++-
 mne/io/tests/test_read_raw.py | 9 ++++++++-
 4 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/examples/io/read_bci2k.py b/examples/io/read_bci2k.py
index 18b3c3b7d7f..a791249fecf 100644
--- a/examples/io/read_bci2k.py
+++ b/examples/io/read_bci2k.py
@@ -6,7 +6,7 @@
 In this example, we use MNE-Python to read a BCI2000 ``.dat`` file.
 BCI2000 is a general-purpose brain-computer interface (BCI) system widely
 used in EEG research. The file is downloaded from the MNE testing data
-repository using :mod:`pooch`.
+repository using ``pooch``.
 
 """  # noqa: D205 D400
 
diff --git a/mne/datasets/config.py b/mne/datasets/config.py
index 455af7eaa0d..0ea542fdca0 100644
--- a/mne/datasets/config.py
+++ b/mne/datasets/config.py
@@ -87,7 +87,7 @@
 # update the checksum in the MNE_DATASETS dict below, and change version
 # here: ↓↓↓↓↓↓↓↓
 RELEASES = dict(
-    testing="0.172",
+    testing="0.173",
     misc="0.27",
     phantom_kit="0.2",
     ucl_opm_auditory="0.2",
@@ -115,7 +115,7 @@
 # Testing and misc are at the top as they're updated most often
 MNE_DATASETS["testing"] = dict(
     archive_name=f"{TESTING_VERSIONED}.tar.gz",
-    hash="md5:9d031f1a91d2bd903a9464ca2cd7dc09",
+    hash="md5:0973c25cd0e3ca43a75ea6953ace1daa",
     url=(
         "https://codeload.github.com/mne-tools/mne-testing-data/"
         f"tar.gz/{RELEASES['testing']}"
diff --git a/mne/io/_read_raw.py b/mne/io/_read_raw.py
index bf1b4e6fae5..e4f2cfacb4a 100644
--- a/mne/io/_read_raw.py
+++ b/mne/io/_read_raw.py
@@ -26,6 +26,7 @@ def _get_supported():
     from . import (
         read_raw_ant,
         read_raw_artemis123,
+        read_raw_bci2k,
         read_raw_bdf,
         read_raw_boxy,
         read_raw_brainvision,
@@ -79,7 +80,10 @@ def _get_supported():
         ".ds": dict(CTF=read_raw_ctf),
         ".txt": dict(BOXY=read_raw_boxy),
         # Curry
-        ".dat": dict(CURRY=read_raw_curry),
+        ".dat": dict(
+            CURRY=read_raw_curry,
+            BCI2K=read_raw_bci2k,
+        ),
         ".dap": dict(CURRY=read_raw_curry),
         ".rs3": dict(CURRY=read_raw_curry),
         ".cdt": dict(CURRY=read_raw_curry),
@@ -130,6 +134,7 @@ def read_raw(fname, *, preload=False, verbose=None, **kwargs) -> BaseRaw:
 
     * `~mne.io.read_raw_ant`
     * `~mne.io.read_raw_artemis123`
+    * `~mne.io.read_raw_bci2k`
     * `~mne.io.read_raw_bdf`
     * `~mne.io.read_raw_boxy`
     * `~mne.io.read_raw_brainvision`
diff --git a/mne/io/tests/test_read_raw.py b/mne/io/tests/test_read_raw.py
index f74a715d9e7..63877c9018d 100644
--- a/mne/io/tests/test_read_raw.py
+++ b/mne/io/tests/test_read_raw.py
@@ -15,6 +15,7 @@
 
 base = Path(__file__).parents[1]
 test_base = Path(testing.data_path(download=False))
+bci2k_fname = test_base / "BCI2k" / "bci2k_test.dat"
 
 
 @pytest.mark.parametrize("fname", ["x.xxx", "x"])
@@ -129,7 +130,7 @@ def test_all_reader_documented():
     missing_from_doc = set(functions) - set(reader_lines)
     if len(missing_from_doc) != 0 or len(missing_from_read_raw) != 0:
         raise AssertionError(
-            "Functions missing from documentation:\n\t"
+            "Functions missing from documentation of mne.io.read_raw:\n\t"
             + "\n\t".join(missing_from_doc)
             + "\n\nFunctions missing from read_raw:\n\t"
             + "\n\t".join(missing_from_read_raw)
@@ -156,3 +157,9 @@ def test_all_reader_documented_in_docstring():
         )
     if sorted(documented) != documented:
         raise AssertionError("Functions in docstring are not sorted.")
+
+
+def test_bci2k():
+    """Test reading BCI2k files with read_raw."""
+    raw = read_raw(bci2k_fname, preload=True)
+    assert "RawBCI2k" in repr(raw)

From b7465c8a8551b420080e11918033a4e9824897bd Mon Sep 17 00:00:00 2001
From: Eric Larson <larson.eric.d@gmail.com>
Date: Fri, 3 Apr 2026 17:29:52 -0400
Subject: [PATCH 23/26] FIX: Exception

---
 mne/tests/test_import_nesting.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mne/tests/test_import_nesting.py b/mne/tests/test_import_nesting.py
index e4525de1247..2d1dfdb35ee 100644
--- a/mne/tests/test_import_nesting.py
+++ b/mne/tests/test_import_nesting.py
@@ -141,7 +141,7 @@ def generic_visit(self, node):
         ("mne/utils/docs.py", "    import mne", "non-relative mne import"),
         (
             "mne/io/_read_raw.py",
-            "    from . import read_raw_ant, read_raw_artemis123, read_raw_bdf, read_raw_boxy, read_raw_brainvision, read_raw_cnt, read_raw_ctf, read_raw_curry, read_raw_edf, read_raw_eeglab, read_raw_egi, read_raw_eximia, read_raw_eyelink, read_raw_fieldtrip, read_raw_fif, read_raw_fil, read_raw_gdf, read_raw_kit, read_raw_mef, read_raw_nedf, read_raw_nicolet, read_raw_nihon, read_raw_nirx, read_raw_nsx, read_raw_persyst, read_raw_snirf",  # noqa: E501
+            "    from . import read_raw_ant, read_raw_artemis123, read_raw_bci2k, read_raw_bdf, read_raw_boxy, read_raw_brainvision, read_raw_cnt, read_raw_ctf, read_raw_curry, read_raw_edf, read_raw_eeglab, read_raw_egi, read_raw_eximia, read_raw_eyelink, read_raw_fieldtrip, read_raw_fif, read_raw_fil, read_raw_gdf, read_raw_kit, read_raw_mef, read_raw_nedf, read_raw_nicolet, read_raw_nihon, read_raw_nirx, read_raw_nsx, read_raw_persyst, read_raw_snirf",  # noqa: E501
             "non-explicit relative import",
         ),
         (

From 63dfae15cf04099bec1572e504a80ba70f09d972 Mon Sep 17 00:00:00 2001
From: Eric Larson <larson.eric.d@gmail.com>
Date: Fri, 3 Apr 2026 17:34:34 -0400
Subject: [PATCH 24/26] FIX: More

---
 doc/sphinxext/related_software_nodeps.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/doc/sphinxext/related_software_nodeps.txt b/doc/sphinxext/related_software_nodeps.txt
index 712e2834496..45c216d72dd 100644
--- a/doc/sphinxext/related_software_nodeps.txt
+++ b/doc/sphinxext/related_software_nodeps.txt
@@ -3,3 +3,4 @@ cross-domain-saliency-maps
 fsleyes
 mne-kit-gui
 mne-videobrowser
+hedtools  # because it limits our Pandas version currently

From 2da83bc4e3b3ef0135fd667c8ffaa92e15cfb560 Mon Sep 17 00:00:00 2001
From: Eric Larson <larson.eric.d@gmail.com>
Date: Fri, 3 Apr 2026 17:56:02 -0400
Subject: [PATCH 25/26] FIX: Better

---
 examples/io/read_bci2k.py     | 3 +--
 mne/io/tests/test_read_raw.py | 3 ++-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/examples/io/read_bci2k.py b/examples/io/read_bci2k.py
index a791249fecf..fbc8ea4877d 100644
--- a/examples/io/read_bci2k.py
+++ b/examples/io/read_bci2k.py
@@ -7,7 +7,6 @@
 BCI2000 is a general-purpose brain-computer interface (BCI) system widely
 used in EEG research. The file is downloaded from the MNE testing data
 repository using ``pooch``.
-
 """  # noqa: D205 D400
 
 # Authors: The MNE-Python contributors.
@@ -19,7 +18,7 @@
 import mne
 
 # %%
-# First, we download the sample BCI2000 ``.dat`` file using :mod:`pooch`.
+# First, we download the sample BCI2000 ``.dat`` file using ``pooch``.
 
 data_dir = mne.datasets.default_path() / "bci2k_data"
 data_dir.mkdir(exist_ok=True)
diff --git a/mne/io/tests/test_read_raw.py b/mne/io/tests/test_read_raw.py
index 63877c9018d..7b3c77f9433 100644
--- a/mne/io/tests/test_read_raw.py
+++ b/mne/io/tests/test_read_raw.py
@@ -10,7 +10,7 @@
 import pytest
 
 from mne.datasets import testing
-from mne.io import read_raw
+from mne.io import read_raw, read_raw_bci2k
 from mne.io._read_raw import _get_readers, _get_supported, split_name_ext
 
 base = Path(__file__).parents[1]
@@ -161,5 +161,6 @@ def test_all_reader_documented_in_docstring():
 
 def test_bci2k():
     """Test reading BCI2k files with read_raw."""
+    read_raw_bci2k(bci2k_fname, preload=True)  # smoke test: if this works, below should
     raw = read_raw(bci2k_fname, preload=True)
     assert "RawBCI2k" in repr(raw)

From 52c16bb33c265bf73a44b2e4ea9df739e1b4a888 Mon Sep 17 00:00:00 2001
From: Eric Larson <larson.eric.d@gmail.com>
Date: Fri, 3 Apr 2026 18:21:42 -0400
Subject: [PATCH 26/26] FIX: mark

---
 mne/io/tests/test_read_raw.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/mne/io/tests/test_read_raw.py b/mne/io/tests/test_read_raw.py
index 7b3c77f9433..b9ff8fde8d8 100644
--- a/mne/io/tests/test_read_raw.py
+++ b/mne/io/tests/test_read_raw.py
@@ -159,6 +159,7 @@ def test_all_reader_documented_in_docstring():
         raise AssertionError("Functions in docstring are not sorted.")
 
 
+@testing.requires_testing_data
 def test_bci2k():
     """Test reading BCI2k files with read_raw."""
     read_raw_bci2k(bci2k_fname, preload=True)  # smoke test: if this works, below should
