Skip to content

Commit 92a3095

Browse files
authored
Merge branch 'master' into fix-spikegadgets
2 parents afebd5d + 32ac313 commit 92a3095

File tree

6 files changed

+69
-44
lines changed

6 files changed

+69
-44
lines changed

codemeta.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
"license": "https://spdx.org/licenses/BSD-3-Clause",
55
"codeRepository": "https://github.com/NeuralEnsemble/python-neo",
66
"contIntegration": "https://github.com/NeuralEnsemble/python-neo/actions",
7-
"dateModified": "2024-08-01",
8-
"downloadUrl": "https://files.pythonhosted.org/packages/0f/16/4e22eb38621183d56acde0abbe591f15e79c6332e9ec360fc5db171b39ab/neo-0.13.2.tar.gz",
7+
"dateModified": "2024-08-28",
8+
"downloadUrl": "https://files.pythonhosted.org/packages/08/4b/c863c6bff783e94c92cb814f6ae821b35e6463c5a66e809b6864d0c66b4e/neo-0.13.3.tar.gz",
99
"issueTracker": "https://github.com/NeuralEnsemble/python-neo/issues",
1010
"name": "Neo",
11-
"version": "0.13.2",
11+
"version": "0.13.3",
1212
"identifier": "RRID:SCR_000634",
1313
"description": "Neo is a Python package for working with electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats, including Spike2, NeuroExplorer, AlphaOmega, Axon, Blackrock, Plexon, Tdt, and support for writing to a subset of these formats plus non-proprietary formats including HDF5.\n\nThe goal of Neo is to improve interoperability between Python tools for analyzing, visualizing and generating electrophysiology data by providing a common, shared object model. In order to be as lightweight a dependency as possible, Neo is deliberately limited to represention of data, with no functions for data analysis or visualization.\n\nNeo is used by a number of other software tools, including SpykeViewer (data analysis and visualization), Elephant (data analysis), the G-node suite (databasing), PyNN (simulations), tridesclous_ (spike sorting) and ephyviewer (data visualization).\n\nNeo implements a hierarchical data model well adapted to intracellular and extracellular electrophysiology and EEG data with support for multi-electrodes (for example tetrodes). Neo's data objects build on the quantities package, which in turn builds on NumPy by adding support for physical dimensions. Thus Neo objects behave just like normal NumPy arrays, but with additional metadata, checks for dimensional consistency and automatic unit conversion.",
1414
"applicationCategory": "neuroscience",
15-
"releaseNotes": "https://neo.readthedocs.io/en/stable/releases/0.13.2.html",
15+
"releaseNotes": "https://neo.readthedocs.io/en/stable/releases/0.13.3.html",
1616
"funding": "https://cordis.europa.eu/project/id/945539",
1717
"developmentStatus": "active",
1818
"referencePublication": "https://doi.org/10.3389/fninf.2014.00010",

neo/rawio/medrawio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, strea
240240
self.sess.set_channel_active(self._stream_info[stream_index]["raw_chans"])
241241
num_channels = len(self._stream_info[stream_index]["raw_chans"])
242242
self.sess.set_reference_channel(self._stream_info[stream_index]["raw_chans"][0])
243-
243+
244244
# in the case we have a slice or we give an ArrayLike we need to iterate through the channels
245245
# in order to activate them.
246246
else:

neo/rawio/plexon2rawio/plexon2rawio.py

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
import pathlib
2626
import warnings
2727
import platform
28+
import re
2829

29-
from collections import namedtuple
3030
from urllib.request import urlopen
3131
from datetime import datetime
3232

@@ -53,6 +53,10 @@ class Plexon2RawIO(BaseRawIO):
5353
pl2_dll_file_path: str | Path | None, default: None
5454
The path to the necessary dll for loading pl2 files
5555
If None will find correct dll for architecture and if it does not exist will download it
56+
reading_attempts: int, default: 15
57+
Number of attempts to read the file before raising an error
58+
This opening process is somewhat unreliable and might fail occasionally. Adjust this higher
59+
if you encounter problems in opening the file.
5660
5761
Notes
5862
-----
@@ -88,7 +92,7 @@ class Plexon2RawIO(BaseRawIO):
8892
extensions = ["pl2"]
8993
rawmode = "one-file"
9094

91-
def __init__(self, filename, pl2_dll_file_path=None):
95+
def __init__(self, filename, pl2_dll_file_path=None, reading_attempts=15):
9296

9397
# signals, event and spiking data will be cached
9498
# cached signal data can be cleared using `clear_analogsignal_cache()()`
@@ -128,7 +132,6 @@ def __init__(self, filename, pl2_dll_file_path=None):
128132

129133
self.pl2reader = PyPL2FileReader(pl2_dll_file_path=pl2_dll_file_path)
130134

131-
reading_attempts = 10
132135
for attempt in range(reading_attempts):
133136
self.pl2reader.pl2_open_file(self.filename)
134137

@@ -152,54 +155,80 @@ def _parse_header(self):
152155
# Scanning sources and populating signal channels at the same time. Sources have to have
153156
# same sampling rate and number of samples to belong to one stream.
154157
signal_channels = []
155-
source_characteristics = {}
156-
Source = namedtuple("Source", "id name sampling_rate n_samples")
157-
for c in range(self.pl2reader.pl2_file_info.m_TotalNumberOfAnalogChannels):
158-
achannel_info = self.pl2reader.pl2_get_analog_channel_info(c)
158+
channel_num_samples = []
159+
160+
# We will build the stream ids based on the channel prefixes
161+
# The channel prefixes are the first characters of the channel names which have the following format:
162+
# WB{number}, FPX{number}, SPKCX{number}, AI{number}, etc
163+
# We will extract the prefix and use it as stream id
164+
regex_prefix_pattern = r"^\D+" # Match any non-digit character at the beginning of the string
165+
166+
for channel_index in range(self.pl2reader.pl2_file_info.m_TotalNumberOfAnalogChannels):
167+
achannel_info = self.pl2reader.pl2_get_analog_channel_info(channel_index)
159168
# only consider active channels
160-
if not achannel_info.m_ChannelEnabled:
169+
if not (achannel_info.m_ChannelEnabled and achannel_info.m_ChannelRecordingEnabled):
161170
continue
162171

163172
# assign to matching stream or create new stream based on signal characteristics
164173
rate = achannel_info.m_SamplesPerSecond
165-
n_samples = achannel_info.m_NumberOfValues
166-
source_id = str(achannel_info.m_Source)
167-
168-
channel_source = Source(source_id, f"stream@{rate}Hz", rate, n_samples)
169-
existing_source = source_characteristics.setdefault(source_id, channel_source)
170-
171-
# ensure that stream of this channel and existing stream have same properties
172-
if channel_source != existing_source:
173-
raise ValueError(
174-
f"The channel source {channel_source} must be the same as the existing source {existing_source}"
175-
)
174+
num_samples = achannel_info.m_NumberOfValues
175+
channel_num_samples.append(num_samples)
176176

177177
ch_name = achannel_info.m_Name.decode()
178178
chan_id = f"source{achannel_info.m_Source}.{achannel_info.m_Channel}"
179179
dtype = "int16"
180180
units = achannel_info.m_Units.decode()
181181
gain = achannel_info.m_CoeffToConvertToUnits
182182
offset = 0.0 # PL2 files don't contain information on signal offset
183-
stream_id = source_id
183+
184+
channel_prefix = re.match(regex_prefix_pattern, ch_name).group(0)
185+
stream_id = channel_prefix
184186
signal_channels.append((ch_name, chan_id, rate, dtype, units, gain, offset, stream_id))
185187

186188
signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype)
187-
self.signal_stream_characteristics = source_characteristics
188-
189-
# create signal streams from source information
189+
channel_num_samples = np.array(channel_num_samples)
190+
191+
# We are using channel prefixes as stream_ids
192+
# The meaning of the channel prefixes was provided by a Plexon Engineer, see here:
193+
# https://github.com/NeuralEnsemble/python-neo/pull/1495#issuecomment-2184256894
194+
stream_id_to_stream_name = {
195+
"WB": "WB-Wideband",
196+
"FP": "FPl-Low Pass Filtered",
197+
"SP": "SPKC-High Pass Filtered",
198+
"AI": "AI-Auxiliary Input",
199+
}
200+
201+
unique_stream_ids = np.unique(signal_channels["stream_id"])
190202
signal_streams = []
191-
for stream_idx, source in source_characteristics.items():
192-
signal_streams.append((source.name, str(source.id)))
203+
for stream_id in unique_stream_ids:
204+
# We are using the channel prefixes as ids
205+
# The users of plexon can modify the prefix of the channel names (e.g. `my_prefix` instead of `WB`).
206+
# In that case we use the channel prefix both as stream id and name
207+
stream_name = stream_id_to_stream_name.get(stream_id, stream_id)
208+
signal_streams.append((stream_name, stream_id))
209+
193210
signal_streams = np.array(signal_streams, dtype=_signal_stream_dtype)
194211

212+
self.stream_id_samples = {}
213+
self.stream_index_to_stream_id = {}
214+
for stream_index, stream_id in enumerate(signal_streams["id"]):
215+
# Keep a mapping from stream_index to stream_id
216+
self.stream_index_to_stream_id[stream_index] = stream_id
217+
218+
# We extract the number of samples for each stream
219+
mask = signal_channels["stream_id"] == stream_id
220+
signal_num_samples = np.unique(channel_num_samples[mask])
221+
assert signal_num_samples.size == 1, "All channels in a stream must have the same number of samples"
222+
self.stream_id_samples[stream_id] = signal_num_samples[0]
223+
195224
# pre-loading spike channel_data for later usage
196225
self._spike_channel_cache = {}
197226
spike_channels = []
198227
for c in range(self.pl2reader.pl2_file_info.m_TotalNumberOfSpikeChannels):
199228
schannel_info = self.pl2reader.pl2_get_spike_channel_info(c)
200229

201230
# only consider active channels
202-
if not schannel_info.m_ChannelEnabled:
231+
if not (schannel_info.m_ChannelEnabled and schannel_info.m_ChannelRecordingEnabled):
203232
continue
204233

205234
for channel_unit_id in range(schannel_info.m_NumberOfUnits):
@@ -223,7 +252,7 @@ def _parse_header(self):
223252
echannel_info = self.pl2reader.pl2_get_digital_channel_info(i)
224253

225254
# only consider active channels
226-
if not echannel_info.m_ChannelEnabled:
255+
if not (echannel_info.m_ChannelEnabled and echannel_info.m_ChannelRecordingEnabled):
227256
continue
228257

229258
# event channels are characterized by (name, id, type), with type in ['event', 'epoch']
@@ -354,16 +383,12 @@ def _segment_t_stop(self, block_index, seg_index):
354383
end_time = (
355384
self.pl2reader.pl2_file_info.m_StartRecordingTime + self.pl2reader.pl2_file_info.m_DurationOfRecording
356385
)
357-
return end_time / self.pl2reader.pl2_file_info.m_TimestampFrequency
386+
return float(end_time / self.pl2reader.pl2_file_info.m_TimestampFrequency)
358387

359388
def _get_signal_size(self, block_index, seg_index, stream_index):
360-
# this must return an integer value (the number of samples)
361-
362-
stream_id = self.header["signal_streams"][stream_index]["id"]
363-
stream_characteristic = list(self.signal_stream_characteristics.values())[stream_index]
364-
if stream_id != stream_characteristic.id:
365-
raise ValueError(f"The `stream_id` must be {stream_characteristic.id}")
366-
return int(stream_characteristic.n_samples) # Avoids returning a numpy.int64 scalar
389+
stream_id = self.stream_index_to_stream_id[stream_index]
390+
num_samples = int(self.stream_id_samples[stream_id])
391+
return num_samples
367392

368393
def _get_signal_t_start(self, block_index, seg_index, stream_index):
369394
# This returns the t_start of signals as a float value in seconds

neo/test/iotest/test_plexonio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class TestPlexonIO(
1818
"plexon/File_plexon_1.plx",
1919
"plexon/File_plexon_2.plx",
2020
"plexon/File_plexon_3.plx",
21-
"plexon/4chDemoPLX.plx"
21+
"plexon/4chDemoPLX.plx",
2222
]
2323

2424

neo/test/rawiotest/rawio_compliance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,15 @@ def read_analogsignals(reader):
224224
i_start=i_start,
225225
i_stop=i_stop,
226226
stream_index=stream_index,
227-
channel_indexes=slice(None)
227+
channel_indexes=slice(None),
228228
)
229229
raw_chunk_channel_indexes = reader.get_analogsignal_chunk(
230230
block_index=block_index,
231231
seg_index=seg_index,
232232
i_start=i_start,
233233
i_stop=i_stop,
234234
stream_index=stream_index,
235-
channel_indexes=channel_indexes
235+
channel_indexes=channel_indexes,
236236
)
237237

238238
np.testing.assert_array_equal(raw_chunk_slice_none, raw_chunk_channel_indexes)

neo/test/rawiotest/test_plexonrawio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class TestPlexonRawIO(
1515
"plexon/File_plexon_1.plx",
1616
"plexon/File_plexon_2.plx",
1717
"plexon/File_plexon_3.plx",
18-
"plexon/4chDemoPLX.plx"
18+
"plexon/4chDemoPLX.plx",
1919
]
2020

2121

0 commit comments

Comments
 (0)