From d7e2009f7d0dc1b0d1b5ad9b6516f436cec5b458 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Tue, 29 Sep 2026 15:44:53 -0600 Subject: [PATCH 1/4] Read the lower word of PLX timestamps as unsigned --- neo/rawio/plexonrawio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo/rawio/plexonrawio.py b/neo/rawio/plexonrawio.py index 5e759f995..0f713fcb0 100644 --- a/neo/rawio/plexonrawio.py +++ b/neo/rawio/plexonrawio.py @@ -694,7 +694,7 @@ def read_as_dict(fid, dtype, offset=None): DataBlockHeader = [ ("Type", "uint16"), ("UpperByteOf5ByteTimestamp", "uint16"), - ("TimeStamp", "int32"), + ("TimeStamp", "uint32"), ("Channel", "uint16"), ("Unit", "uint16"), ("NumberOfWaveforms", "uint16"), From dd6b9f997c6b4d1a70165a364e6628560142f520 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Mon, 5 Oct 2026 15:31:41 -0600 Subject: [PATCH 2/4] Test reading PLX timestamps whose lower word is above 2**31 --- neo/test/rawiotest/test_plexonrawio.py | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/neo/test/rawiotest/test_plexonrawio.py b/neo/test/rawiotest/test_plexonrawio.py index 86207e904..6025d4af1 100644 --- a/neo/test/rawiotest/test_plexonrawio.py +++ b/neo/test/rawiotest/test_plexonrawio.py @@ -1,5 +1,7 @@ import unittest +import numpy as np + from neo.rawio.plexonrawio import PlexonRawIO from neo.test.rawiotest.common_rawio_test import BaseTestRawIO @@ -16,8 +18,34 @@ class TestPlexonRawIO( "plexon/File_plexon_2.plx", "plexon/File_plexon_3.plx", "plexon/4chDemoPLX.plx", + "plexon/plx/timestamp_clock/one_megahertz_forty_bit_timestamps/sorted_spikes_and_strobed_events.plx", ] + def test_timestamps_with_lower_word_above_two_to_the_31(self): + """The lower 32 bits of the 40-bit block timestamp must be read unsigned. + + A PLX block stores its timestamp as an upper byte and a lower 32-bit word, and the timestamp is + upper * 2 ** 32 + lower. The reader used to read the lower word as a signed int32, so a lower word at + or above 2 ** 31 came out negative and the timestamp landed 2 ** 32 ticks early. At the usual 40 kHz + clock that only happens after about 15 hours, but this file has a 1 MHz clock (an Offline Sorter + import of a Neuralynx recording), where it starts after 36 minutes and each affected timestamp comes + out about 72 minutes early. + + The expected values are the spikes of one unit on both sides of the second wrap, at 2 * 2 ** 32 ticks, + decoded from the block headers. The two before the wrap have lower words above 2 ** 31 and are the + ones the signed read moved; the two after it have small lower words and were always right. + """ + filename = self.get_local_path( + "plexon/plx/timestamp_clock/one_megahertz_forty_bit_timestamps/sorted_spikes_and_strobed_events.plx" + ) + reader = PlexonRawIO(filename=filename) + reader.parse_header() + + ids = list(reader.header["spike_channels"]["id"]) + spike_timestamps = reader.get_spike_timestamps(spike_channel_index=ids.index("ch33#1")) + around_second_wrap = spike_timestamps[(spike_timestamps > 8_589_760_000) & (spike_timestamps < 8_590_600_000)] + np.testing.assert_array_equal(around_second_wrap, [8_589_765_078, 8_589_773_845, 8_590_497_111, 8_590_594_911]) + if __name__ == "__main__": unittest.main() From f50195ef7ab10afaf96afe441c32580c4fb0fac3 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Mon, 5 Oct 2026 17:23:28 -0600 Subject: [PATCH 3/4] Show how PLX timestamps are encoded in the test docstring --- neo/test/rawiotest/test_plexonrawio.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/neo/test/rawiotest/test_plexonrawio.py b/neo/test/rawiotest/test_plexonrawio.py index 6025d4af1..f1dac1cff 100644 --- a/neo/test/rawiotest/test_plexonrawio.py +++ b/neo/test/rawiotest/test_plexonrawio.py @@ -24,12 +24,22 @@ class TestPlexonRawIO( def test_timestamps_with_lower_word_above_two_to_the_31(self): """The lower 32 bits of the 40-bit block timestamp must be read unsigned. - A PLX block stores its timestamp as an upper byte and a lower 32-bit word, and the timestamp is - upper * 2 ** 32 + lower. The reader used to read the lower word as a signed int32, so a lower word at - or above 2 ** 31 came out negative and the timestamp landed 2 ** 32 ticks early. At the usual 40 kHz - clock that only happens after about 15 hours, but this file has a 1 MHz clock (an Offline Sorter - import of a Neuralynx recording), where it starts after 36 minutes and each affected timestamp comes - out about 72 minutes early. + A PLX block stores its 40-bit timestamp in two fields of its header: + + +-------------+------------------------------------+ + | upper byte | lower word | + | (8 bits) | (32 bits) | + +-------------+------------------------------------+ + how many ticks since the last wrap + wraps (0 to 2 ** 32 - 1, then back to 0) + + timestamp = upper * 2 ** 32 + lower + e.g. upper = 1, lower = 4_294_797_782 -> 8_589_765_078 ticks (8589.77 s at 1 MHz) + + The reader used to read the lower word as a signed int32, so a lower word at or above 2 ** 31 came out + negative and the timestamp landed 2 ** 32 ticks early. At the usual 40 kHz clock that only happens after + about 15 hours, but this file has a 1 MHz clock (an Offline Sorter import of a Neuralynx recording), + where it starts after 36 minutes and each affected timestamp comes out about 72 minutes early. The expected values are the spikes of one unit on both sides of the second wrap, at 2 * 2 ** 32 ticks, decoded from the block headers. The two before the wrap have lower words above 2 ** 31 and are the From 7e518e4becc90304955ea38e76bce79850d19b52 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 8 Oct 2026 11:08:58 +0200 Subject: [PATCH 4/4] test: fix datalad git-annex version --- .github/workflows/caches_cron_job.yml | 3 ++- .github/workflows/io-test.yml | 3 ++- .github/workflows/plexon2-testing.yml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/caches_cron_job.yml b/.github/workflows/caches_cron_job.yml index 4b1046aee..f5cde1cca 100644 --- a/.github/workflows/caches_cron_job.yml +++ b/.github/workflows/caches_cron_job.yml @@ -94,7 +94,8 @@ jobs: git config --global user.name "neo CI" python -m pip install -U pip # Official recommended way pip install datalad-installer - datalad-installer --sudo ok git-annex --method datalad/packages + # TODO: remove the pin once datalad uploads the git-annex 10.20261006 package (its latest-version points to a missing file) + datalad-installer --sudo ok git-annex=10.20260901 --method datalad/packages pip install datalad git config --global filter.annex.process "git-annex filter-process" # recommended for efficiency diff --git a/.github/workflows/io-test.yml b/.github/workflows/io-test.yml index 5815d3cd6..68a741680 100644 --- a/.github/workflows/io-test.yml +++ b/.github/workflows/io-test.yml @@ -96,7 +96,8 @@ jobs: shell: bash run: | pip install datalad-installer - datalad-installer --sudo ok git-annex --method datalad/packages + # TODO: remove the pin once datalad uploads the git-annex 10.20261006 package (its latest-version points to a missing file) + datalad-installer --sudo ok git-annex=10.20260901 --method datalad/packages git config --global filter.annex.process "git-annex filter-process" # recommended for efficiency - name: Configure git diff --git a/.github/workflows/plexon2-testing.yml b/.github/workflows/plexon2-testing.yml index 28005f404..1c66377ef 100644 --- a/.github/workflows/plexon2-testing.yml +++ b/.github/workflows/plexon2-testing.yml @@ -68,7 +68,8 @@ jobs: shell: bash run: | pip install datalad-installer - datalad-installer --sudo ok git-annex --method datalad/packages + # TODO: remove the pin once datalad uploads the git-annex 10.20261006 package (its latest-version points to a missing file) + datalad-installer --sudo ok git-annex=10.20260901 --method datalad/packages git config --global filter.annex.process "git-annex filter-process" # recommended for efficiency - name: Configure git