Skip to content

Commit fa39967

Browse files
authored
Merge pull request #2952 from NinelK/sinaps
Add extractors for SiNAPS Research Platform
2 parents 04120b2 + bfb42c5 commit fa39967

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed

src/spikeinterface/extractors/extractorlist.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
from .herdingspikesextractors import HerdingspikesSortingExtractor, read_herdingspikes
4646
from .mdaextractors import MdaRecordingExtractor, MdaSortingExtractor, read_mda_recording, read_mda_sorting
4747
from .phykilosortextractors import PhySortingExtractor, KiloSortSortingExtractor, read_phy, read_kilosort
48+
from .sinapsrecordingextractors import (
49+
SinapsResearchPlatformRecordingExtractor,
50+
SinapsResearchPlatformH5RecordingExtractor,
51+
read_sinaps_research_platform,
52+
read_sinaps_research_platform_h5,
53+
)
4854

4955
# sorting in relation with simulator
5056
from .shybridextractors import (
@@ -77,6 +83,7 @@
7783
CompressedBinaryIblExtractor,
7884
IblRecordingExtractor,
7985
MCSH5RecordingExtractor,
86+
SinapsResearchPlatformRecordingExtractor,
8087
]
8188
recording_extractor_full_list += neo_recording_extractors_list
8289

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
from __future__ import annotations
2+
3+
import warnings
4+
from pathlib import Path
5+
import numpy as np
6+
7+
from probeinterface import get_probe
8+
9+
from ..core import BaseRecording, BaseRecordingSegment, BinaryRecordingExtractor, ChannelSliceRecording
10+
from ..core.core_tools import define_function_from_class
11+
12+
13+
class SinapsResearchPlatformRecordingExtractor(ChannelSliceRecording):
14+
"""
15+
Recording extractor for the SiNAPS research platform system saved in binary format.
16+
17+
Parameters
18+
----------
19+
file_path : str | Path
20+
Path to the SiNAPS .bin file.
21+
stream_name : "filt" | "raw" | "aux", default: "filt"
22+
The stream name to extract.
23+
"filt" extracts the filtered data, "raw" extracts the raw data, and "aux" extracts the auxiliary data.
24+
"""
25+
26+
extractor_name = "SinapsResearchPlatform"
27+
mode = "file"
28+
name = "sinaps_research_platform"
29+
30+
def __init__(self, file_path: str | Path, stream_name: str = "filt"):
31+
from ..preprocessing import UnsignedToSignedRecording
32+
33+
file_path = Path(file_path)
34+
meta_file = file_path.parent / f"metadata_{file_path.stem}.txt"
35+
meta = parse_sinaps_meta(meta_file)
36+
37+
num_aux_channels = meta["nbHWAux"] + meta["numberUserAUX"]
38+
num_total_channels = 2 * meta["nbElectrodes"] + num_aux_channels
39+
num_electrodes = meta["nbElectrodes"]
40+
sampling_frequency = meta["samplingFreq"]
41+
42+
probe_type = meta["probeType"]
43+
num_bits = int(np.log2(meta["nbADCLevels"]))
44+
45+
gain_ephys = meta["voltageConverter"]
46+
gain_aux = meta["voltageAUXConverter"]
47+
48+
recording = BinaryRecordingExtractor(
49+
file_path, sampling_frequency, dtype="uint16", num_channels=num_total_channels
50+
)
51+
recording = UnsignedToSignedRecording(recording, bit_depth=num_bits)
52+
53+
if stream_name == "raw":
54+
channel_slice = recording.channel_ids[:num_electrodes]
55+
renamed_channels = np.arange(num_electrodes)
56+
gain = gain_ephys
57+
elif stream_name == "filt":
58+
channel_slice = recording.channel_ids[num_electrodes : 2 * num_electrodes]
59+
renamed_channels = np.arange(num_electrodes)
60+
gain = gain_ephys
61+
elif stream_name == "aux":
62+
channel_slice = recording.channel_ids[2 * num_electrodes :]
63+
hw_chans = meta["hwAUXChannelName"][1:-1].split(",")
64+
user_chans = meta["userAuxName"][1:-1].split(",")
65+
renamed_channels = hw_chans + user_chans
66+
gain = gain_aux
67+
else:
68+
raise ValueError("stream_name must be 'raw', 'filt', or 'aux'")
69+
70+
ChannelSliceRecording.__init__(self, recording, channel_ids=channel_slice, renamed_channel_ids=renamed_channels)
71+
72+
self.set_channel_gains(gain)
73+
self.set_channel_offsets(0)
74+
num_channels = self.get_num_channels()
75+
76+
if (stream_name == "filt") | (stream_name == "raw"):
77+
probe = get_sinaps_probe(probe_type, num_channels)
78+
if probe is not None:
79+
self.set_probe(probe, in_place=True)
80+
81+
self._kwargs = {"file_path": str(file_path.absolute()), "stream_name": stream_name}
82+
83+
84+
class SinapsResearchPlatformH5RecordingExtractor(BaseRecording):
85+
"""
86+
Recording extractor for the SiNAPS research platform system saved in HDF5 format.
87+
88+
Parameters
89+
----------
90+
file_path : str | Path
91+
Path to the SiNAPS .h5 file.
92+
"""
93+
94+
extractor_name = "SinapsResearchPlatformH5"
95+
mode = "file"
96+
name = "sinaps_research_platform_h5"
97+
98+
def __init__(self, file_path: str | Path):
99+
self._file_path = file_path
100+
101+
sinaps_info = parse_sinapse_h5(self._file_path)
102+
self._rf = sinaps_info["filehandle"]
103+
104+
BaseRecording.__init__(
105+
self,
106+
sampling_frequency=sinaps_info["sampling_frequency"],
107+
channel_ids=sinaps_info["channel_ids"],
108+
dtype=sinaps_info["dtype"],
109+
)
110+
111+
self.extra_requirements.append("h5py")
112+
113+
recording_segment = SiNAPSH5RecordingSegment(
114+
self._rf,
115+
sinaps_info["num_frames"],
116+
sampling_frequency=sinaps_info["sampling_frequency"],
117+
num_bits=sinaps_info["num_bits"],
118+
)
119+
self.add_recording_segment(recording_segment)
120+
121+
# set gain
122+
self.set_channel_gains(sinaps_info["gain"])
123+
self.set_channel_offsets(sinaps_info["offset"])
124+
self.num_bits = sinaps_info["num_bits"]
125+
num_channels = self.get_num_channels()
126+
127+
# set probe
128+
probe = get_sinaps_probe(sinaps_info["probe_type"], num_channels)
129+
if probe is not None:
130+
self.set_probe(probe, in_place=True)
131+
132+
self._kwargs = {"file_path": str(Path(file_path).absolute())}
133+
134+
def __del__(self):
135+
self._rf.close()
136+
137+
138+
class SiNAPSH5RecordingSegment(BaseRecordingSegment):
139+
def __init__(self, rf, num_frames, sampling_frequency, num_bits):
140+
BaseRecordingSegment.__init__(self, sampling_frequency=sampling_frequency)
141+
self._rf = rf
142+
self._num_samples = int(num_frames)
143+
self._num_bits = num_bits
144+
self._stream = self._rf.require_group("RealTimeProcessedData")
145+
146+
def get_num_samples(self):
147+
return self._num_samples
148+
149+
def get_traces(self, start_frame=None, end_frame=None, channel_indices=None):
150+
if isinstance(channel_indices, slice):
151+
traces = self._stream.get("FilteredData")[channel_indices, start_frame:end_frame].T
152+
else:
153+
# channel_indices is np.ndarray
154+
if np.array(channel_indices).size > 1 and np.any(np.diff(channel_indices) < 0):
155+
# get around h5py constraint that it does not allow datasets
156+
# to be indexed out of order
157+
sorted_channel_indices = np.sort(channel_indices)
158+
resorted_indices = np.array([list(sorted_channel_indices).index(ch) for ch in channel_indices])
159+
recordings = self._stream.get("FilteredData")[sorted_channel_indices, start_frame:end_frame].T
160+
traces = recordings[:, resorted_indices]
161+
else:
162+
traces = self._stream.get("FilteredData")[channel_indices, start_frame:end_frame].T
163+
# convert uint16 to int16 here to simplify extractor
164+
if traces.dtype == "uint16":
165+
dtype_signed = "int16"
166+
# upcast to int with double itemsize
167+
signed_dtype = "int32"
168+
offset = 2 ** (self._num_bits - 1)
169+
traces = traces.astype(signed_dtype, copy=False) - offset
170+
traces = traces.astype(dtype_signed, copy=False)
171+
return traces
172+
173+
174+
read_sinaps_research_platform = define_function_from_class(
175+
source_class=SinapsResearchPlatformRecordingExtractor, name="read_sinaps_research_platform"
176+
)
177+
178+
read_sinaps_research_platform_h5 = define_function_from_class(
179+
source_class=SinapsResearchPlatformH5RecordingExtractor, name="read_sinaps_research_platform_h5"
180+
)
181+
182+
183+
##############################################
184+
# HELPER FUNCTIONS
185+
##############################################
186+
187+
188+
def get_sinaps_probe(probe_type, num_channels):
189+
try:
190+
probe = get_probe(manufacturer="sinaps", probe_name=f"SiNAPS-{probe_type}")
191+
# now wire the probe
192+
channel_indices = np.arange(num_channels)
193+
probe.set_device_channel_indices(channel_indices)
194+
return probe
195+
except:
196+
warnings.warn(f"Could not load probe information for {probe_type}")
197+
return None
198+
199+
200+
def parse_sinaps_meta(meta_file):
201+
meta_dict = {}
202+
with open(meta_file) as f:
203+
lines = f.readlines()
204+
for l in lines:
205+
if "**" in l or "=" not in l:
206+
continue
207+
else:
208+
key, val = l.split("=")
209+
val = val.replace("\n", "")
210+
try:
211+
val = int(val)
212+
except:
213+
pass
214+
try:
215+
val = eval(val)
216+
except:
217+
pass
218+
meta_dict[key] = val
219+
return meta_dict
220+
221+
222+
def parse_sinapse_h5(filename):
223+
"""Open an SiNAPS hdf5 file, read and return the recording info."""
224+
225+
import h5py
226+
227+
rf = h5py.File(filename, "r")
228+
229+
stream = rf.require_group("RealTimeProcessedData")
230+
data = stream.get("FilteredData")
231+
dtype = data.dtype
232+
233+
parameters = rf.require_group("Parameters")
234+
gain = parameters.get("VoltageConverter")[0]
235+
offset = 0
236+
237+
nRecCh, nFrames = data.shape
238+
239+
samplingRate = parameters.get("SamplingFrequency")[0]
240+
241+
probe_type = str(
242+
rf.require_group("Advanced Recording Parameters").require_group("Probe").get("probeType").asstr()[...]
243+
)
244+
num_bits = int(
245+
np.log2(rf.require_group("Advanced Recording Parameters").require_group("DAQ").get("nbADCLevels")[0])
246+
)
247+
248+
sinaps_info = {
249+
"filehandle": rf,
250+
"num_frames": nFrames,
251+
"sampling_frequency": samplingRate,
252+
"num_channels": nRecCh,
253+
"channel_ids": np.arange(nRecCh),
254+
"gain": gain,
255+
"offset": offset,
256+
"dtype": dtype,
257+
"probe_type": probe_type,
258+
"num_bits": num_bits,
259+
}
260+
261+
return sinaps_info

0 commit comments

Comments
 (0)