Skip to content

Commit 60b26d4

Browse files
authored
Merge pull request #1541 from h-mayorquin/fixate_plexon2_streams
Fix-ate plexon 2 streams
2 parents 0aa596e + 058bce4 commit 60b26d4

File tree

1 file changed

+57
-32
lines changed

1 file changed

+57
-32
lines changed

neo/rawio/plexon2rawio/plexon2rawio.py

Lines changed: 57 additions & 32 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,46 +155,72 @@ 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
160169
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 = []
@@ -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

0 commit comments

Comments
 (0)