Skip to content

Commit ba7e716

Browse files
authored
Merge branch 'master' into fix_intan_one_file_per_channel
2 parents c158e55 + 830c461 commit ba7e716

File tree

9 files changed

+115
-35
lines changed

9 files changed

+115
-35
lines changed

.github/workflows/core-test.yml

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,43 @@ jobs:
2525
fail-fast: true
2626
matrix:
2727
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
28-
python-version: ['3.9', '3.10', '3.11', '3.12']
29-
numpy-version: ['1.22.4', '1.23.5', '1.24.1', '1.25.1', '1.26.4']
28+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
29+
numpy-version: ['1.22.4', '1.23.5', '1.24.4', '1.25.1', '1.26.4', '2.0.2','2.1']
3030
# numpy 1.22: 3.10, 1.23: 3.11, 1.24: 3.11, 1.25: 3.11, 1.26: 3.12
3131
exclude:
32+
- python-version: '3.9'
33+
numpy-version: '2.1'
3234
- python-version: '3.11'
3335
numpy-version: '1.22.4'
3436
- python-version: '3.12'
3537
numpy-version: '1.22.4'
3638
- python-version: '3.12'
3739
numpy-version: '1.23.5'
3840
- python-version: '3.12'
39-
numpy-version: '1.24.1'
41+
numpy-version: '1.24.4'
4042
- python-version: '3.12'
4143
numpy-version: '1.25.1'
44+
- python-version: '3.13'
45+
numpy-version: '1.22.4'
46+
- python-version: '3.13'
47+
numpy-version: '1.23.5'
48+
- python-version: '3.13'
49+
numpy-version: '1.24.4'
50+
- python-version: '3.13'
51+
numpy-version: '1.25.1'
52+
- python-version: '3.13'
53+
numpy-version: '1.26.4'
54+
- python-version: '3.13'
55+
numpy-version: '2.0.2'
56+
4257
steps:
4358
- name: Set up Python ${{ matrix.python-version }}
44-
uses: actions/setup-python@v4
59+
uses: actions/setup-python@v5
4560
with:
4661
python-version: ${{ matrix.python-version }}
4762

4863
- name: Checkout repository
49-
uses: actions/checkout@v3
64+
uses: actions/checkout@v4
5065

5166
- name: Install numpy ${{ matrix.numpy-version }}
5267
run: |

.github/workflows/io-test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ jobs:
1818
strategy:
1919
fail-fast: true
2020
matrix:
21-
python-version: ['3.9', '3.11.9']
21+
python-version: ['3.9', '3.12']
2222
defaults:
2323
# by default run in bash mode (required for conda usage)
2424
run:
2525
shell: bash -l {0}
2626
steps:
2727

2828
- name: Checkout repository
29-
uses: actions/checkout@v3
29+
uses: actions/checkout@v4
3030

3131
- name: Get current year-month
3232
id: date
@@ -38,7 +38,7 @@ jobs:
3838
run: |
3939
echo "dataset_hash=$(git ls-remote https://gin.g-node.org/NeuralEnsemble/ephy_testing_data.git HEAD | cut -f1)" >> $GITHUB_OUTPUT
4040
41-
- uses: actions/cache/restore@v3
41+
- uses: actions/cache/restore@v4
4242
# Loading cache of ephys_testing_dataset
4343
id: cache-datasets
4444
with:

environment_testing.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ channels:
44
dependencies:
55
- datalad
66
- pip
7+
# temporary have this here for IO testing while we decide how to deal with
8+
# external packages not 2.0 ready
9+
- numpy=1.26.4

neo/core/spiketrain.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,7 @@ def _merge_array_annotations(self, others, sorting=None):
979979

980980
omitted_keys_other = [
981981
key
982-
for key in np.unique([key for other in others for key in other.array_annotations])
982+
for key in set([key for other in others for key in other.array_annotations])
983983
if key not in self.array_annotations
984984
]
985985

neo/rawio/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* :attr:`MicromedRawIO`
3030
* :attr:`NeuralynxRawIO`
3131
* :attr:`NeuroExplorerRawIO`
32-
* :attr:`NeuroNexusRawIO
32+
* :attr:`NeuroNexusRawIO`
3333
* :attr:`NeuroScopeRawIO`
3434
* :attr:`NIXRawIO`
3535
* :attr:`OpenEphysRawIO`

neo/rawio/openephysbinaryrawio.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ def _parse_header(self):
217217
if name + "_npy" in info:
218218
data = np.load(info[name + "_npy"], mmap_mode="r")
219219
info[name] = data
220-
221220
# check that events have timestamps
222221
assert "timestamps" in info, "Event stream does not have timestamps!"
223222
# Updates for OpenEphys v0.6:
@@ -253,30 +252,64 @@ def _parse_header(self):
253252
# 'states' was introduced in OpenEphys v0.6. For previous versions, events used 'channel_states'
254253
if "states" in info or "channel_states" in info:
255254
states = info["channel_states"] if "channel_states" in info else info["states"]
255+
256256
if states.size > 0:
257257
timestamps = info["timestamps"]
258258
labels = info["labels"]
259-
rising = np.where(states > 0)[0]
260-
falling = np.where(states < 0)[0]
261259

262-
# infer durations
260+
# Identify unique channels based on state values
261+
channels = np.unique(np.abs(states))
262+
263+
rising_indices = []
264+
falling_indices = []
265+
266+
# all channels are packed into the same `states` array.
267+
# So the states array includes positive and negative values for each channel:
268+
# for example channel one rising would be +1 and channel one falling would be -1,
269+
# channel two rising would be +2 and channel two falling would be -2, etc.
270+
# This is the case for sure for version >= 0.6.x.
271+
for channel in channels:
272+
# Find rising and falling edges for each channel
273+
rising = np.where(states == channel)[0]
274+
falling = np.where(states == -channel)[0]
275+
276+
# Ensure each rising has a corresponding falling
277+
if rising.size > 0 and falling.size > 0:
278+
if rising[0] > falling[0]:
279+
falling = falling[1:]
280+
if rising.size > falling.size:
281+
rising = rising[:-1]
282+
283+
# ensure that the number of rising and falling edges are the same:
284+
if len(rising) != len(falling):
285+
warn(
286+
f"Channel {channel} has {len(rising)} rising edges and "
287+
f"{len(falling)} falling edges. The number of rising and "
288+
f"falling edges should be equal. Skipping events from this channel."
289+
)
290+
continue
291+
292+
rising_indices.extend(rising)
293+
falling_indices.extend(falling)
294+
295+
rising_indices = np.array(rising_indices)
296+
falling_indices = np.array(falling_indices)
297+
298+
# Sort the indices to maintain chronological order
299+
sorted_order = np.argsort(rising_indices)
300+
rising_indices = rising_indices[sorted_order]
301+
falling_indices = falling_indices[sorted_order]
302+
263303
durations = None
264-
if len(states) > 0:
265-
# make sure first event is rising and last is falling
266-
if states[0] < 0:
267-
falling = falling[1:]
268-
if states[-1] > 0:
269-
rising = rising[:-1]
270-
271-
if len(rising) == len(falling):
272-
durations = timestamps[falling] - timestamps[rising]
273-
if not self._use_direct_evt_timestamps:
274-
timestamps = timestamps / info["sample_rate"]
275-
durations = durations / info["sample_rate"]
276-
277-
info["rising"] = rising
278-
info["timestamps"] = timestamps[rising]
279-
info["labels"] = labels[rising]
304+
# if len(rising_indices) == len(falling_indices):
305+
durations = timestamps[falling_indices] - timestamps[rising_indices]
306+
if not self._use_direct_evt_timestamps:
307+
timestamps = timestamps / info["sample_rate"]
308+
durations = durations / info["sample_rate"]
309+
310+
info["rising"] = rising_indices
311+
info["timestamps"] = timestamps[rising_indices]
312+
info["labels"] = labels[rising_indices]
280313
info["durations"] = durations
281314

282315
# no spike read yet

neo/test/coretest/test_spiketrain.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ def test__create_from_quantity_array(self):
313313

314314
def test__create_from_quantity_array_with_dtype(self):
315315
times = np.arange(10, dtype="f4") * pq.ms
316+
# this step is required for NumPy 2.0 which now casts to float64 in the case either value/array
317+
# is float64 even if not necessary
318+
# https://numpy.org/devdocs/numpy_2_0_migration_guide.html
319+
times = times.astype(dtype="f4")
316320
t_start = 0.0 * pq.s
317321
t_stop = 12.0 * pq.ms
318322
train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop)
@@ -343,6 +347,10 @@ def test__create_from_quantity_array_no_start_stop_units(self):
343347

344348
def test__create_from_quantity_array_no_start_stop_units_with_dtype(self):
345349
times = np.arange(10, dtype="f4") * pq.ms
350+
# this step is required for NumPy 2.0 which now casts to float64 in the case either value/array
351+
# is float64 even if not necessary
352+
# https://numpy.org/devdocs/numpy_2_0_migration_guide.html
353+
times = times.astype(dtype="f4")
346354
t_start = 0.0
347355
t_stop = 12.0
348356
train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop)
@@ -1143,7 +1151,7 @@ def test_merge_multiple(self):
11431151
expected *= time_unit
11441152
sorting = np.argsort(expected)
11451153
expected = expected[sorting]
1146-
np.testing.assert_array_equal(result.times, expected)
1154+
np.testing.assert_array_equal(result.times.magnitude, expected.magnitude)
11471155

11481156
# Make sure array annotations are merged correctly
11491157
self.assertTrue("label" not in result.array_annotations)

neo/test/rawiotest/test_openephysbinaryrawio.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from neo.rawio.openephysbinaryrawio import OpenEphysBinaryRawIO
44
from neo.test.rawiotest.common_rawio_test import BaseTestRawIO
55

6+
import numpy as np
7+
68

79
class TestOpenEphysBinaryRawIO(BaseTestRawIO, unittest.TestCase):
810
rawioclass = OpenEphysBinaryRawIO
@@ -57,6 +59,25 @@ def test_missing_folders(self):
5759
)
5860
rawio.parse_header()
5961

62+
def test_multiple_ttl_events_parsing(self):
63+
rawio = OpenEphysBinaryRawIO(
64+
self.get_local_path("openephysbinary/v0.6.x_neuropixels_with_sync"), load_sync_channel=False
65+
)
66+
rawio.parse_header()
67+
rawio.header = rawio.header
68+
# Testing co
69+
# This is the TTL events from the NI Board channel
70+
ttl_events = rawio._evt_streams[0][0][1]
71+
assert "rising" in ttl_events.keys()
72+
assert "labels" in ttl_events.keys()
73+
assert "durations" in ttl_events.keys()
74+
assert "timestamps" in ttl_events.keys()
75+
76+
# Check that durations of different event streams are correctly parsed:
77+
assert np.allclose(ttl_events["durations"][ttl_events["labels"] == "1"], 0.5, atol=0.001)
78+
assert np.allclose(ttl_events["durations"][ttl_events["labels"] == "6"], 0.025, atol=0.001)
79+
assert np.allclose(ttl_events["durations"][ttl_events["labels"] == "7"], 0.016666, atol=0.001)
80+
6081

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

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.14.0.dev0"
44
authors = [{name = "Neo authors and contributors"}]
55
description = "Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats"
66
readme = "README.rst"
7-
requires-python = ">=3.9,<3.13" # 3.13 will require NumPy > 2.0 (Windows issue in CI)
7+
requires-python = ">=3.9"
88
license = {text = "BSD 3-Clause License"}
99
classifiers = [
1010
"Development Status :: 4 - Beta",
@@ -23,7 +23,7 @@ classifiers = [
2323

2424
dependencies = [
2525
"packaging",
26-
"numpy>=1.22.4,<2.0.0",
26+
"numpy>=1.22.4",
2727
"quantities>=0.16.1"
2828
]
2929

@@ -45,7 +45,7 @@ iocache = [
4545
]
4646

4747
test = [
48-
"dhn_med_py<2.0", # ci failing with 2.0 test future version when stable
48+
# "dhn_med_py<2.0", # ci failing with 2.0 test future version when stable
4949
"pytest",
5050
"pytest-cov",
5151
# datalad # this dependency is covered by conda (environment_testing.yml)
@@ -105,7 +105,7 @@ plexon2 = ["zugbruecke>=0.2; sys_platform!='win32'", "wenv; sys_platform!='win32
105105
all = [
106106
"coverage",
107107
"coveralls",
108-
"dhn_med_py<2.0", # ci failing with 2.0 test future version when stable
108+
# "dhn_med_py<2.0", # ci failing with 2.0 test future version when stable
109109
"h5py",
110110
"igor2",
111111
"ipython",

0 commit comments

Comments
 (0)