From c13ac605bf392a8bfaae13c89765ba8f3ac5536a Mon Sep 17 00:00:00 2001 From: aaronkanzer Date: Sat, 24 Oct 2026 07:57:04 -0400 Subject: [PATCH 1/3] Read BrainVision marker positions as 1-based The BrainVision marker file counts data points from 1, so a marker at position 1 is on the first sample. BrainVisionRawIO used the position as a 0-based sample index, which put every event one sample late. --- neo/rawio/brainvisionrawio.py | 3 +- neo/test/rawiotest/test_brainvisionrawio.py | 49 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/neo/rawio/brainvisionrawio.py b/neo/rawio/brainvisionrawio.py index 601381470..6fdf6f8a6 100644 --- a/neo/rawio/brainvisionrawio.py +++ b/neo/rawio/brainvisionrawio.py @@ -166,7 +166,8 @@ def _parse_header(self): for i in range(len(all_info)): ev_type, ev_label, pos, size, channel = all_info[f"Mk{i+1}"].split(",")[:5] ev_types.append(ev_type) - ev_timestamps.append(int(pos)) + # marker positions are 1-based: the first data point is position 1 + ev_timestamps.append(int(pos) - 1) ev_labels.append(ev_label) ev_types = np.array(ev_types) ev_timestamps = np.array(ev_timestamps) diff --git a/neo/test/rawiotest/test_brainvisionrawio.py b/neo/test/rawiotest/test_brainvisionrawio.py index b582b4103..2399a9668 100644 --- a/neo/test/rawiotest/test_brainvisionrawio.py +++ b/neo/test/rawiotest/test_brainvisionrawio.py @@ -1,6 +1,10 @@ """ """ +import tempfile import unittest +from pathlib import Path + +import numpy as np from neo.rawio.brainvisionrawio import BrainVisionRawIO @@ -24,5 +28,50 @@ class TestBrainVisionRawIO( entities_to_download = ["brainvision"] +class TestBrainVisionRawIOMarkers(unittest.TestCase): + def test_marker_positions_are_one_based(self): + vhdr = ( + "Brain Vision Data Exchange Header File Version 1.0\n" + "[Common Infos]\n" + "Codepage=UTF-8\n" + "DataFile=test.eeg\n" + "MarkerFile=test.vmrk\n" + "DataFormat=BINARY\n" + "DataOrientation=MULTIPLEXED\n" + "NumberOfChannels=1\n" + "SamplingInterval=1000\n" + "[Binary Infos]\n" + "BinaryFormat=INT_16\n" + "[Channel Infos]\n" + "Ch1=Fp1,,1,µV\n" + ) + vmrk = ( + "Brain Vision Data Exchange Marker File, Version 1.0\n" + "[Marker Infos]\n" + "Mk1=New Segment,,1,1,0\n" + "Mk2=Stimulus,S 1,11,1,0\n" + ) + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_dir = Path(tmp_dir) + (tmp_dir / "test.vhdr").write_text(vhdr, encoding="utf8") + (tmp_dir / "test.vmrk").write_text(vmrk, encoding="utf8") + np.arange(20, dtype="int16").tofile(tmp_dir / "test.eeg") + + reader = BrainVisionRawIO(filename=tmp_dir / "test.vhdr") + reader.parse_header() + event_names = list(reader.header["event_channels"]["name"]) + + # position 1 is the first data point, so it is sample 0 and time 0 s + timestamps, _, _ = reader.get_event_timestamps(event_channel_index=event_names.index("New Segment")) + np.testing.assert_array_equal(timestamps, [0]) + + # position 11 is sample 10, 10 ms into the recording at 1 kHz + timestamps, _, labels = reader.get_event_timestamps(event_channel_index=event_names.index("Stimulus")) + np.testing.assert_array_equal(timestamps, [10]) + np.testing.assert_array_equal(labels, ["S 1"]) + times = reader.rescale_event_timestamp(timestamps, event_channel_index=event_names.index("Stimulus")) + np.testing.assert_allclose(times, [0.010]) + + if __name__ == "__main__": unittest.main() From 580c40e27f2ca0d1f6004163d438cac4ad1d39e2 Mon Sep 17 00:00:00 2001 From: aaronkanzer Date: Sat, 24 Oct 2026 07:57:04 -0400 Subject: [PATCH 2/3] Add Aaron Kanzer to the author list --- doc/source/authors.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/authors.rst b/doc/source/authors.rst index 035fdfbe7..678548332 100644 --- a/doc/source/authors.rst +++ b/doc/source/authors.rst @@ -106,6 +106,7 @@ and may not be the current affiliation of a contributor. * Aditya Singh (github) * Stanley C. * Axel Coiffard-Donaty +* Aaron Kanzer [53] 1. Centre de Recherche en Neuroscience de Lyon, CNRS UMR5292 - INSERM U1028 - Université Claude Bernard Lyon 1 2. Unité de Neuroscience, Information et Complexité, CNRS UPR 3293, Gif-sur-Yvette, France @@ -159,6 +160,7 @@ and may not be the current affiliation of a contributor. 50. Ludwig-Maximilians-Universität München, Munich, Germany 51. Stony Brook University, Stony Brook, NY, USA 52. University of Sussex, Brighton, UK +53. Scigantic, USA From 486cdf64c2e475d5a174e9e9a5e7929ebe70727e Mon Sep 17 00:00:00 2001 From: aaronkanzer Date: Sat, 24 Oct 2026 15:58:32 -0400 Subject: [PATCH 3/3] Cite the BrainVision spec for 1-based marker positions --- neo/rawio/brainvisionrawio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/neo/rawio/brainvisionrawio.py b/neo/rawio/brainvisionrawio.py index 6fdf6f8a6..50158995b 100644 --- a/neo/rawio/brainvisionrawio.py +++ b/neo/rawio/brainvisionrawio.py @@ -166,7 +166,9 @@ def _parse_header(self): for i in range(len(all_info)): ev_type, ev_label, pos, size, channel = all_info[f"Mk{i+1}"].split(",")[:5] ev_types.append(ev_type) - # marker positions are 1-based: the first data point is position 1 + # Marker positions are 1-based (the first data point is position 1), see + # section 4.3 of the BrainVision Core Data Format 1.0 specification: + # https://www.brainproducts.com/download/specification-of-brainvision-core-data-format-1-0/ ev_timestamps.append(int(pos) - 1) ev_labels.append(ev_label) ev_types = np.array(ev_types)