Skip to content

Commit 27e3111

Browse files
authored
Merge pull request #382 from int-brain-lab/pykilosort
Pykilosort
2 parents 5addaae + 64e88f1 commit 27e3111

File tree

9 files changed

+482
-21
lines changed

9 files changed

+482
-21
lines changed

ibllib/dsp/voltage.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,19 @@ def destripe(x, fs, tr_sel=None, neuropixel_version=1, butter_kwargs=None, k_kwa
210210
return x_
211211

212212

213-
def decompress_destripe_cbin(sr, output_file=None, h=None):
213+
def decompress_destripe_cbin(sr, output_file=None, h=None, wrot=None, append=False, nc_out=None, ns2add=0):
214214
"""
215-
From a spikeglx Reader object, decompresses and apply ADC
215+
From a spikeglx Reader object, decompresses and apply ADC.
216+
Saves output as a flat binary file in int16
216217
Production version with optimized FFTs - requires pyfftw
217218
:param sr: seismic reader object (spikeglx.Reader)
218219
:param output_file: (optional, defaults to .bin extension of the compressed bin file)
219220
:param h: (optional)
221+
:param wrot: (optional) whitening matrix [nc x nc] or amplitude scalar to apply to the output
222+
:param append: (optional, False) for chronic recordings, append to end of file
223+
:param nc_out: (optional, True) saves non selected channels (synchronisation trace) in output
224+
:param ns2add: (optional) for kilosort, adds padding samples at the end of the file so the total
225+
number of samples is a multiple of the batchsize
220226
:return:
221227
"""
222228
import pyfftw
@@ -235,6 +241,7 @@ def decompress_destripe_cbin(sr, output_file=None, h=None):
235241
taper = np.r_[0, scipy.signal.windows.cosine((SAMPLES_TAPER - 1) * 2), 0]
236242
# create the FFT stencils
237243
ncv = h['x'].size # number of channels
244+
nc_out = nc_out or sr.nc
238245
# compute LP filter coefficients
239246
sos = scipy.signal.butter(**butter_kwargs, output='sos')
240247
# compute fft stencil for batchsize
@@ -247,7 +254,7 @@ def decompress_destripe_cbin(sr, output_file=None, h=None):
247254
DEPHAS = np.exp(1j * np.angle(fft_object(dephas)) * h['sample_shift'][:, np.newaxis])
248255

249256
pbar = tqdm(total=sr.ns / sr.fs)
250-
with open(output_file, 'wb') as fid:
257+
with open(output_file, 'ab' if append else 'wb') as fid:
251258
first_s = 0
252259
while True:
253260
last_s = np.minimum(NBATCH + first_s, sr.ns)
@@ -273,10 +280,14 @@ def decompress_destripe_cbin(sr, output_file=None, h=None):
273280
chunk = kfilt(chunk, **k_kwargs)
274281
# add back sync trace and save
275282
chunk = np.r_[chunk, sr[first_s:last_s, ncv:].T].T
276-
(chunk[slice(*ind2save), :] / sr.channel_conversion_sample2v['ap']
277-
).astype(np.int16).tofile(fid)
283+
chunk = chunk[slice(*ind2save), :] / sr.channel_conversion_sample2v['ap']
284+
if wrot is not None:
285+
chunk[:, :ncv] = np.dot(chunk[:, :ncv], wrot)
286+
chunk[:, :nc_out].astype(np.int16).tofile(fid)
278287
first_s += NBATCH - SAMPLES_TAPER * 2
279288
pbar.update(NBATCH / sr.fs)
280289
if last_s == sr.ns:
290+
if ns2add > 0:
291+
np.tile(chunk[-1, :nc_out].astype(np.int16), (ns2add, 1)).tofile(fid)
281292
break
282293
pbar.close()

ibllib/ephys/spikes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ def _sr(ap_file):
140140
np.save(st_file, interp_times)
141141
# get the list of output files
142142
out_files.extend([f for f in out_path.glob("*.*") if
143-
f.name.startswith(('channels.', 'clusters.', 'spikes.', 'templates.',
144-
'_kilosort_', '_phy_spikes_subset'))])
143+
f.name.startswith(('channels.', 'drift', 'clusters.', 'spikes.', 'templates.',
144+
'_kilosort_', '_phy_spikes_subset', '_ibl_log.info'))])
145145
return out_files, 0
146146

147147

ibllib/io/extractors/signatures.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# RAW DATA
2+
RAW_BEHAVIOUR = [
3+
('_iblrig_ambientSensorData.raw', 'raw_behavior_data', True),
4+
('_iblrig_encoderEvents.raw', 'raw_behavior_data', True),
5+
('_iblrig_encoderPositions.raw', 'raw_behavior_data', True),
6+
('_iblrig_encoderTrialInfo.raw', 'raw_behavior_data', True),
7+
('_iblrig_micData.raw', 'raw_behavior_data', False),
8+
('_iblrig_stimPositionScreen.raw', 'raw_behavior_data', False),
9+
('_iblrig_syncSquareUpdate.raw', 'raw_behavior_data', False),
10+
('_iblrig_taskData.raw', 'raw_behavior_data', True),
11+
('_iblrig_taskSettings.raw', 'raw_behavior_data', True),
12+
]
13+
14+
RAW_PASSIVE = [
15+
('_iblrig_RFMapStim.raw', 'raw_passive_data', True),
16+
('_iblrig_stimPositionScreen.raw', 'raw_passive_data', False),
17+
('_iblrig_syncSquareUpdate.raw', 'raw_passive_data', False),
18+
('_iblrig_taskSettings.raw', 'raw_passive_data', False),
19+
('_iblrig_encoderEvents.raw', 'raw_passive_data', False),
20+
('_iblrig_encoderPositions.raw', 'raw_passive_data', False),
21+
('_iblrig_encoderTrialInfo.raw', 'raw_passive_data', False),
22+
]
23+
24+
# Data common to both 3A and 3B
25+
RAW_EPHYS = [
26+
['ephysData.raw.ap', 'raw_ephys_data/XX', True],
27+
['ephysData.raw.ch', 'raw_ephys_data/XX', True, ['ap', 'lf']],
28+
['ephysData.raw.lf', 'raw_ephys_data/XX', True],
29+
['ephysData.raw.meta', 'raw_ephys_data/XX', True, ['ap', 'lf']],
30+
]
31+
32+
# These are for both 3B probes but only main probe for 3A
33+
RAW_EPHYS_EXTRA = [
34+
['ephysData.raw.sync', 'raw_ephys_data/XX', True],
35+
['ephysData.raw.timestamps', 'raw_ephys_data/XX', True],
36+
['ephysData.raw.wiring', 'raw_ephys_data/XX', False],
37+
]
38+
39+
# Only for 3B
40+
RAW_EPHYS_NIDAQ = [
41+
('ephysData.raw.nidq', 'raw_ephys_data', True),
42+
('ephysData.raw.meta', 'raw_ephys_data', True, ['nidq']),
43+
('ephysData.raw.ch', 'raw_ephys_data', True, ['nidq']),
44+
]
45+
46+
47+
RAW_VIDEO = [
48+
('_iblrig_Camera.raw', 'raw_video_data', True, ['left', 'right', 'body']),
49+
('_iblrig_Camera.timestamps', 'raw_video_data', True, ['left', 'right', 'body']),
50+
('_iblrig_Camera.GPIO', 'raw_video_data', False, ['left', 'right', 'body']),
51+
('_iblrig_Camera.frame_counter', 'raw_video_data', False, ['left', 'right', 'body']),
52+
]
53+
54+
# PROCESSED DATA
55+
TRIALS = [
56+
('trials.choice', 'alf', True),
57+
('trials.contrastLeft', 'alf', True),
58+
('trials.contrastRight', 'alf', True),
59+
('trials.feedbackType', 'alf', True),
60+
('trials.feedback_times', 'alf', True),
61+
('trials.firstMovement_times', 'alf', True),
62+
('trials.goCueTrigger_times', 'alf', True),
63+
('trials.goCue_times', 'alf', True),
64+
('trials.intervals', 'alf', True),
65+
('trials.intervals', 'alf', True),
66+
('trials.probabilityLeft', 'alf', True),
67+
('trials.response_times', 'alf', True),
68+
('trials.rewardVolume', 'alf', True),
69+
('trials.stimOff_times', 'alf', True),
70+
('trials.stimOn_times', 'alf', True),
71+
]
72+
73+
WHEEL = [
74+
('wheel.position', 'alf', True),
75+
('wheel.timestamps', 'alf', True),
76+
('wheelMoves.intervals', 'alf', True),
77+
('wheelMoves.peakAmplitude', 'alf', True),
78+
]
79+
80+
PASSIVE = [
81+
('_ibl_passiveGabor.table', 'alf', True),
82+
('_ibl_passivePeriods.intervalsTable', 'alf', True),
83+
('_ibl_passiveRFM.times', 'alf', True),
84+
('_ibl_passiveStims.table', 'alf', True),
85+
]
86+
87+
88+
DLC = [
89+
('camera.dlc', 'alf', True, ['left', 'right', 'body']),
90+
('camera.times', 'alf', True, ['left', 'right', 'body']),
91+
('camera.ROIMotionEnergy', 'alf', False, ['left', 'right', 'body']),
92+
('ROIMotionEnergy.position', 'alf', False, ['left', 'right', 'body']),
93+
]
94+
95+
VIDEO = [
96+
('camera.times', 'alf', True, ['left', 'right', 'body']),
97+
]
98+
99+
# Data common to both 3A and 3B
100+
EPHYS = [
101+
['_spikeglx_sync.channels', 'raw_ephys_data/XX', True],
102+
['_spikeglx_sync.polarities', 'raw_ephys_data/XX', True],
103+
['_spikeglx_sync.times', 'raw_ephys_data/XX', True],
104+
['_iblqc_ephysSpectralDensity.freqs', 'raw_ephys_data/XX', True, ['ap', 'lf']],
105+
['_iblqc_ephysSpectralDensity.power', 'raw_ephys_data/XX', True, ['ap', 'lf']],
106+
['_iblqc_ephysTimeRms.rms', 'raw_ephys_data/XX', True, ['ap', 'lf']],
107+
['_iblqc_ephysTimeRms.timestamps', 'raw_ephys_data/XX', True, ['ap', 'lf']],
108+
]
109+
110+
# Only for 3B
111+
EPHYS_NIDAQ = [
112+
('_spikeglx_sync.channels', 'raw_ephys_data', True),
113+
('_spikeglx_sync.polarities', 'raw_ephys_data', True),
114+
('_spikeglx_sync.times', 'raw_ephys_data', True),
115+
]
116+
117+
SPIKE_SORTING = [
118+
['_phy_spikes_subset.channels', 'alf/XX', False],
119+
['_phy_spikes_subset.spikes', 'alf/XX', False],
120+
['_phy_spikes_subset.waveforms', 'alf/XX', False],
121+
['channels.brainLocationIds_ccf_2017', 'alf/XX', False],
122+
['channels.mlapdv', 'alf/XX', False],
123+
['channels.localCoordinates', 'alf/XX', True],
124+
['channels.rawInd', 'alfa/XX', True],
125+
['clusters.amps', 'alf/XX', True],
126+
['clusters.brainLocationAcronyms_ccf_2017', 'alf/XX', False],
127+
['clusters.brainLocationIds_ccf_2017', 'alf/XX', False],
128+
['clusters.channels', 'alf/XX', True],
129+
['clusters.depths', 'alf/XX', True],
130+
['clusters.metrics', 'alf/XX', False],
131+
['clusters.mlapdv', 'alf/XX', False],
132+
['clusters.peakToTrough', 'alf/XX', True],
133+
['clusters.uuids', 'alf/XX', True],
134+
['clusters.waveforms', 'alf/XX', True],
135+
['clusters.waveformsChannels', 'alf/XX', True],
136+
['spikes.amps', 'alf/XX', True],
137+
['spikes.clusters', 'alf/XX', True],
138+
['spikes.depths', 'alf/XX', True],
139+
['spikes.samples', 'alf/XX', True],
140+
['spikes.templates', 'alf/XX', True],
141+
['spikes.times', 'alf/XX', True],
142+
['templates.amps', 'alf/XX', True],
143+
['templates.waveforms', 'alf/XX', True],
144+
['templates.waveformsChannels', 'alf/XX', True],
145+
]
146+
147+
148+
# RAW DATA TASKS
149+
RAW_BEHAVIOUR_TASKS = ['TrainingRegisterRaw', 'EphysAudio']
150+
RAW_PASSIVE_TASKS = ['TrainingRegisterRaw']
151+
RAW_EPHYS_TASKS = ['EphysMtscomp']
152+
RAW_VIDEO_TASKS = ['TrainingRegisterRaw', 'EphysVideoCompress']
153+
154+
# PROCESSED DATA TASKS
155+
PASSIVE_TASKS = ['EphysPassive']
156+
EPHYS_TASKS = ['EphysPulses', 'RawEphysQC']
157+
VIDEO_TASKS = ['EphysVideoCompress']
158+
TRIAL_TASKS = ['EphysTrials']
159+
WHEEL_TASKS = ['EphysTrials']
160+
SPIKE_SORTING_TASKS = ['SpikeSorting', 'EphysCellsQc']
161+
DLC_TASKS = ['EphysDLC']
162+
163+
# DATA REQUIRED FOR TASKS
164+
EPHYSTRIALS = [('_iblrig_taskData.raw.*', 'raw_behavior_data', True),
165+
('_iblrig_taskSettings.raw.*', 'raw_behavior_data', True),
166+
('_spikeglx_sync.npy', 'raw_ephys_data/**', True),
167+
('_spikeglx_sync.polarities.npy', 'raw_ephys_data/**', True),
168+
('_spikeglx_sync.times.npy', 'raw_ephys_data/**', True),
169+
('_iblrig_encoderEvents.raw', 'raw_behavior_data', True),
170+
('_iblrig_encoderPositions.raw', 'raw_behavior_data', True),
171+
('*wiring.json', 'raw_ephys_data/**', False),
172+
('*.meta', 'raw_ephys_data/**', True)]
173+
174+
EPHYSPASSIVE = [('_iblrig_taskSettings.raw*', 'raw_behavior_data', True),
175+
('_spikeglx_sync.channels.*', 'raw_ephys_data*', True),
176+
('_spikeglx_sync.polarities.*', 'raw_ephys_data*', True),
177+
('_spikeglx_sync.times.*', 'raw_ephys_data*', True),
178+
('*.meta', 'raw_ephys_data*', True),
179+
('_iblrig_RFMapStim.raw*', 'raw_passive_data', True)]
180+
181+
RAWEPHYSQC = [('*.meta', 'raw_ephys_data/probe*', True),
182+
('*.ch', 'raw_ephys_data/probe*', True),
183+
('*.cbin', 'raw_ephys_data/probe*', True)]
184+
185+
186+
def spike_sorting_signature(pname=None):
187+
pname = pname if pname is not None else "probe*"
188+
signature = [('*ap.meta', f'raw_ephys_data/{pname}', True),
189+
('*ap.ch', f'raw_ephys_data/{pname}', True),
190+
('*ap.cbin', f'raw_ephys_data/{pname}', True),
191+
('_spikeglx_sync.channels.*', 'raw_ephys_data*', True),
192+
('_spikeglx_sync.polarities.*', 'raw_ephys_data*', True),
193+
('_spikeglx_sync.times.*', 'raw_ephys_data*', True),
194+
('_iblrig_taskData.raw.*', 'raw_behavior_data', True),
195+
('_iblrig_taskSettings.raw.*', 'raw_behavior_data', True)
196+
]
197+
return signature
198+
199+
200+
SPIKESORTING = [spike_sorting_signature()]

ibllib/oneibl/aws.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import logging
2+
from pathlib import Path
3+
import sys
4+
from time import time
5+
6+
import boto3
7+
import numpy as np
8+
9+
from one.api import ONE
10+
from one.alf.files import add_uuid_string
11+
from iblutil.io.parquet import np2str
12+
13+
14+
_logger = logging.getLogger('ibllib')
15+
16+
AWS_ROOT_PATH = Path('/mnt/ibl')
17+
BUCKET_NAME = 'ibl-brain-wide-map-private'
18+
19+
# To get aws credentials follow
20+
# https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html to install aws cli
21+
# https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html to set up
22+
# credentials
23+
24+
25+
class AWS:
26+
def __init__(self, s3_bucket_name=None, one=None):
27+
# TODO some initialisation routine to set up credentials for the first time
28+
29+
s3 = boto3.resource('s3')
30+
self.bucket_name = s3_bucket_name or BUCKET_NAME
31+
self.bucket = s3.Bucket(self.bucket_name)
32+
self.one = one or ONE()
33+
34+
def _download_datasets(self, datasets):
35+
36+
for _, d in datasets.iterrows():
37+
rel_file_path = Path(d['session_path']).joinpath(d['rel_path'])
38+
file_path = Path(self.one.cache_dir).joinpath(rel_file_path)
39+
file_path.parent.mkdir(exist_ok=True, parents=True)
40+
41+
if file_path.exists():
42+
# already downloaded, need to have some options for overwrite, clobber, look
43+
# for file mismatch like in ONE
44+
_logger.info(f'{file_path} already exists wont redownload')
45+
continue
46+
47+
aws_path = AWS_ROOT_PATH.joinpath(add_uuid_string(rel_file_path,
48+
np2str(np.r_[d.name[0], d.name[1]])))
49+
aws_path = as_aws_path(aws_path)
50+
file_path = as_aws_path(file_path)
51+
# maybe should avoid this and do a try catch instead?, see here
52+
# https://boto3.amazonaws.com/v1/documentation/api/latest/guide/collections.html#filtering
53+
# probably better to do filter on collection ? Not for today
54+
objects = list(self.bucket.objects.filter(Prefix=aws_path))
55+
if len(objects) == 1:
56+
ts = time()
57+
_logger.info(f'Downloading {aws_path} to {file_path}')
58+
self.bucket.download_file(aws_path, file_path)
59+
_logger.debug(f'Complete. Time elapsed {time() - ts} for {file_path}')
60+
else:
61+
_logger.warning(f'{aws_path} not found on s3 bucket: {self.bucket.name}')
62+
63+
64+
def as_aws_path(path):
65+
"""
66+
Convert a path into one suitable for the aws. Mainly for windows to convert // to \
67+
68+
:param path: A Path instance
69+
:return: A formatted path string
70+
71+
"""
72+
if sys.platform == 'win32':
73+
path = '/'.join(str(path).split('\\'))
74+
else:
75+
path = str(path)
76+
77+
return path

0 commit comments

Comments
 (0)