Skip to content

Commit 6e96e9f

Browse files
authored
Merge pull request #410 from htwangtw/read-spike
Add support for Spike2 files
2 parents d2a54ff + fea0e0d commit 6e96e9f

File tree

4 files changed

+133
-0
lines changed

4 files changed

+133
-0
lines changed

phys2bids/io.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,88 @@ def load_gep(filename):
538538
timeseries = [t_ch, trigger]
539539
timeseries.extend(data)
540540
return BlueprintInput(timeseries, freq, names, units, 1)
541+
542+
543+
def load_smr(filename, chtrig=0):
544+
"""Load Spike2 smr file and populate object phys_input.
545+
546+
Parameters
547+
----------
548+
filename: str
549+
Path to the spike smr or smrx file.
550+
551+
chtrig : int
552+
Index of trigger channel.
553+
554+
Returns
555+
-------
556+
BlueprintInput
557+
558+
Note
559+
----
560+
Index of chtrig is 1-index (i.e. spike2 channel number).
561+
562+
See Also
563+
--------
564+
physio_obj.BlueprintInput
565+
"""
566+
import sonpy
567+
568+
# taken from sonpy demo
569+
read_data = {
570+
sonpy.lib.DataType.Adc: sonpy.lib.SonFile.ReadInts,
571+
sonpy.lib.DataType.EventFall: sonpy.lib.SonFile.ReadEvents,
572+
sonpy.lib.DataType.EventRise: sonpy.lib.SonFile.ReadEvents,
573+
sonpy.lib.DataType.EventBoth: sonpy.lib.SonFile.ReadEvents,
574+
sonpy.lib.DataType.Marker: sonpy.lib.SonFile.ReadMarkers,
575+
sonpy.lib.DataType.AdcMark: sonpy.lib.SonFile.ReadWaveMarks,
576+
sonpy.lib.DataType.RealMark: sonpy.lib.SonFile.ReadRealMarks,
577+
sonpy.lib.DataType.TextMark: sonpy.lib.SonFile.ReadTextMarks,
578+
sonpy.lib.DataType.RealWave: sonpy.lib.SonFile.ReadFloats,
579+
}
580+
581+
smrfile = sonpy.lib.SonFile(filename, True)
582+
time_base = smrfile.GetTimeBase()
583+
n_channels = smrfile.MaxChannels()
584+
freq, names, units, timeseries = [], [], [], []
585+
for i in range(n_channels):
586+
current_channel = smrfile.ChannelType(i)
587+
max_n_tick = smrfile.ChannelMaxTime(i)
588+
if current_channel != sonpy.lib.DataType.Off and max_n_tick > 0:
589+
max_n_tick = smrfile.ChannelMaxTime(i)
590+
sample_rate = smrfile.GetIdealRate(i)
591+
if current_channel == sonpy.lib.DataType.Adc:
592+
divide = smrfile.ChannelDivide(i)
593+
else: # marker channels
594+
divide = 1 / (time_base * sample_rate)
595+
# conversion factor from CED spike2 doc
596+
# http://ced.co.uk/img/Spike9.pdf
597+
gain = smrfile.GetChannelScale(i) / 6553.6
598+
offset = smrfile.GetChannelOffset(i)
599+
name = smrfile.GetChannelTitle(chan=i)
600+
unit = smrfile.GetChannelUnits(chan=i)
601+
602+
n_samples = int(np.floor((max_n_tick) / divide))
603+
raw_signal = read_data[current_channel](
604+
smrfile, chan=i, nMax=n_samples, tFrom=0, tUpto=max_n_tick
605+
)
606+
607+
signal = np.array(raw_signal) * gain + offset
608+
609+
# save the data
610+
freq.append(sample_rate)
611+
names.append(name)
612+
units.append(unit)
613+
timeseries.append(signal)
614+
615+
# use the channel with highest sample rate to create time stamps
616+
idx_max = np.argmax(freq)
617+
n_timepoints = len(timeseries[idx_max]) # end point included
618+
time = np.arange(n_timepoints) * freq[idx_max]
619+
620+
# prepend to the existing list
621+
freq = [freq[idx_max]] + freq
622+
timeseries = [time] + timeseries
623+
units = ["s"] + units
624+
names = ["time"] + names
625+
return BlueprintInput(timeseries, freq, names, units, chtrig)

phys2bids/tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,13 @@ def ge_badfiles(testpath):
138138
tmp = fetch_file("tdmyn", testpath, "PPGData_epiRT_columnscsv_00_00_000")
139139
tmp = fetch_file("b6skq", testpath, "PPGData_epiRT_columnstsv_00_00_000")
140140
return fetch_file("8235b", testpath, "PPGData_epiRT_string0000_00_00_000")
141+
142+
143+
@pytest.fixture
144+
def spike2_smrx_file(testpath):
145+
return fetch_file("7x5qw", testpath, "Test_ppg_pulse_spike2.smrx")
146+
147+
148+
@pytest.fixture
149+
def spike2_smr_file(testpath):
150+
return fetch_file("zdpfr", testpath, "Test_ppg_pulse_spike2.smr")

phys2bids/tests/test_io.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import math
22
import os
3+
import sys
34

45
import numpy as np
56
import pytest
@@ -214,3 +215,37 @@ def test_load_gep_two_files_resp(ge_two_gep_files_resp, testpath):
214215
gep_data2 = np.loadtxt(os.path.join(testpath, "PPGData_epiRT_0000000000_00_00_000.gep"))
215216
assert np.array_equal(gep_data1, phys_obj.timeseries[2])
216217
assert np.array_equal(gep_data2, phys_obj.timeseries[3])
218+
219+
220+
@pytest.mark.skipif(
221+
sys.version_info < (3, 7) or sys.version_info > (3, 9),
222+
reason="Requires python between 3.7 and 3.9",
223+
)
224+
@pytest.mark.xfail(reason="We need to fix sonpy install")
225+
def test_load_smr(spike2_smr_file, spike2_smrx_file):
226+
chtrig = 5
227+
228+
# 32-bit file
229+
phys_obj = io.load_smr(spike2_smr_file, chtrig)
230+
assert phys_obj.ch_name[0] == "time"
231+
assert phys_obj.freq[0] == 1000.0
232+
assert phys_obj.units[0] == "s"
233+
234+
# checks that the scanner strigger is in the right channel
235+
# the marker channels are stored as binary
236+
assert phys_obj.ch_name[chtrig] == "Scan Vol"
237+
assert phys_obj.freq[chtrig] == 200.0
238+
assert phys_obj.units[chtrig] == ""
239+
assert len(phys_obj.timeseries[chtrig]) == 60
240+
241+
# 64-bit file should have the same
242+
phys_obj = io.load_smr(spike2_smrx_file, chtrig)
243+
assert phys_obj.ch_name[0] == "time"
244+
assert phys_obj.freq[0] == 1000.0
245+
assert phys_obj.units[0] == "s"
246+
# checks that the scanner trigger is in the right channel
247+
# the marker channels are stored as binary
248+
assert phys_obj.ch_name[chtrig] == "Scan Vol"
249+
assert phys_obj.freq[chtrig] == 200.0
250+
assert phys_obj.units[chtrig] == ""
251+
assert len(phys_obj.timeseries[chtrig]) == 60

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ packages = find:
3636
include_package_data = True
3737

3838
[options.extras_require]
39+
spike2 =
40+
sonpy >=1.7.5;python_version=='3.7.*,3.8.*,3.9.*'
3941
acq =
4042
bioread >=1.0.5
4143
mat=
@@ -55,6 +57,7 @@ style =
5557
interfaces =
5658
%(acq)s
5759
%(mat)s
60+
%(spike2)s
5861
test =
5962
pytest >=5.3
6063
pytest-cov

0 commit comments

Comments
 (0)