Skip to content

Commit f2dd922

Browse files
committed
Fix conflict with master an plexon2
2 parents a977897 + 60b26d4 commit f2dd922

File tree

1 file changed

+61
-39
lines changed

1 file changed

+61
-39
lines changed

neo/rawio/plexon2rawio/plexon2rawio.py

Lines changed: 61 additions & 39 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

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

92-
def __init__(self, filename, pl2_dll_file_path=None):
96+
def __init__(self, filename, pl2_dll_file_path=None, reading_attempts=15):
9397

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

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

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

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

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

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

188190
signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype)
189-
self.signal_stream_characteristics = source_characteristics
190-
191-
# create signal streams from source information
192-
# we consider stream = source but buffer is unkown
191+
channel_num_samples = np.array(channel_num_samples)
192+
193+
# We are using channel prefixes as stream_ids
194+
# The meaning of the channel prefixes was provided by a Plexon Engineer, see here:
195+
# https://github.com/NeuralEnsemble/python-neo/pull/1495#issuecomment-2184256894
196+
stream_id_to_stream_name = {
197+
"WB": "WB-Wideband",
198+
"FP": "FPl-Low Pass Filtered",
199+
"SP": "SPKC-High Pass Filtered",
200+
"AI": "AI-Auxiliary Input",
201+
}
202+
203+
unique_stream_ids = np.unique(signal_channels["stream_id"])
193204
signal_streams = []
194-
for stream_idx, source in source_characteristics.items():
195-
stream_id = source_id = str(source.id)
196-
buffer_id = ""
197-
signal_streams.append((source.name, stream_id, buffer_id))
198-
signal_buffers = np.array([], dtype=_signal_buffer_dtype)
205+
for stream_id in unique_stream_ids:
206+
# We are using the channel prefixes as ids
207+
# The users of plexon can modify the prefix of the channel names (e.g. `my_prefix` instead of `WB`).
208+
# In that case we use the channel prefix both as stream id and name
209+
stream_name = stream_id_to_stream_name.get(stream_id, stream_id)
210+
signal_streams.append((stream_name, stream_id))
199211
signal_streams = np.array(signal_streams, dtype=_signal_stream_dtype)
212+
# In plexon buffer is unkown
213+
signal_buffers = np.array([], dtype=_signal_buffer_dtype)
214+
215+
self.stream_id_samples = {}
216+
self.stream_index_to_stream_id = {}
217+
for stream_index, stream_id in enumerate(signal_streams["id"]):
218+
# Keep a mapping from stream_index to stream_id
219+
self.stream_index_to_stream_id[stream_index] = stream_id
220+
221+
# We extract the number of samples for each stream
222+
mask = signal_channels["stream_id"] == stream_id
223+
signal_num_samples = np.unique(channel_num_samples[mask])
224+
assert signal_num_samples.size == 1, "All channels in a stream must have the same number of samples"
225+
self.stream_id_samples[stream_id] = signal_num_samples[0]
200226

201227
# pre-loading spike channel_data for later usage
202228
self._spike_channel_cache = {}
@@ -205,7 +231,7 @@ def _parse_header(self):
205231
schannel_info = self.pl2reader.pl2_get_spike_channel_info(c)
206232

207233
# only consider active channels
208-
if not schannel_info.m_ChannelEnabled:
234+
if not (schannel_info.m_ChannelEnabled and schannel_info.m_ChannelRecordingEnabled):
209235
continue
210236

211237
for channel_unit_id in range(schannel_info.m_NumberOfUnits):
@@ -229,7 +255,7 @@ def _parse_header(self):
229255
echannel_info = self.pl2reader.pl2_get_digital_channel_info(i)
230256

231257
# only consider active channels
232-
if not echannel_info.m_ChannelEnabled:
258+
if not (echannel_info.m_ChannelEnabled and echannel_info.m_ChannelRecordingEnabled):
233259
continue
234260

235261
# event channels are characterized by (name, id, type), with type in ['event', 'epoch']
@@ -361,16 +387,12 @@ def _segment_t_stop(self, block_index, seg_index):
361387
end_time = (
362388
self.pl2reader.pl2_file_info.m_StartRecordingTime + self.pl2reader.pl2_file_info.m_DurationOfRecording
363389
)
364-
return end_time / self.pl2reader.pl2_file_info.m_TimestampFrequency
390+
return float(end_time / self.pl2reader.pl2_file_info.m_TimestampFrequency)
365391

366392
def _get_signal_size(self, block_index, seg_index, stream_index):
367-
# this must return an integer value (the number of samples)
368-
369-
stream_id = self.header["signal_streams"][stream_index]["id"]
370-
stream_characteristic = list(self.signal_stream_characteristics.values())[stream_index]
371-
if stream_id != stream_characteristic.id:
372-
raise ValueError(f"The `stream_id` must be {stream_characteristic.id}")
373-
return int(stream_characteristic.n_samples) # Avoids returning a numpy.int64 scalar
393+
stream_id = self.stream_index_to_stream_id[stream_index]
394+
num_samples = int(self.stream_id_samples[stream_id])
395+
return num_samples
374396

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

0 commit comments

Comments
 (0)