labdata 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- labdata/__init__.py +17 -0
- labdata/cli.py +499 -0
- labdata/compute/__init__.py +27 -0
- labdata/compute/ec2.py +198 -0
- labdata/compute/ephys.py +469 -0
- labdata/compute/pose.py +281 -0
- labdata/compute/schedulers.py +194 -0
- labdata/compute/singularity.py +95 -0
- labdata/compute/utils.py +561 -0
- labdata/copy.py +351 -0
- labdata/rules/__init__.py +78 -0
- labdata/rules/ephys.py +188 -0
- labdata/rules/imaging.py +618 -0
- labdata/rules/utils.py +290 -0
- labdata/s3.py +317 -0
- labdata/schema/__init__.py +24 -0
- labdata/schema/ephys.py +547 -0
- labdata/schema/general.py +647 -0
- labdata/schema/histology.py +309 -0
- labdata/schema/onephoton.py +93 -0
- labdata/schema/procedures.py +102 -0
- labdata/schema/tasks.py +66 -0
- labdata/schema/twophoton.py +142 -0
- labdata/schema/utils.py +25 -0
- labdata/schema/video.py +243 -0
- labdata/stacks.py +182 -0
- labdata/utils.py +598 -0
- labdata/widgets.py +412 -0
- labdata-0.0.3.dist-info/METADATA +42 -0
- labdata-0.0.3.dist-info/RECORD +36 -0
- labdata-0.0.3.dist-info/WHEEL +5 -0
- labdata-0.0.3.dist-info/entry_points.txt +2 -0
- labdata-0.0.3.dist-info/licenses/LICENSE +674 -0
- labdata-0.0.3.dist-info/top_level.txt +2 -0
- labdata_frontend/Home.py +39 -0
- labdata_frontend/__init__.py +0 -0
labdata/schema/ephys.py
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
from .general import *
|
|
2
|
+
from .procedures import *
|
|
3
|
+
|
|
4
|
+
@dataschema
|
|
5
|
+
class Probe(dj.Manual):
|
|
6
|
+
definition = '''
|
|
7
|
+
probe_id : varchar(32) # probe id to keep track or re-uses
|
|
8
|
+
---
|
|
9
|
+
probe_type : varchar(12) # probe type
|
|
10
|
+
probe_n_shanks : tinyint # number of shanks
|
|
11
|
+
probe_recording_channels : int # number of recording channels
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
@dataschema
|
|
15
|
+
class ProbeInsertion(dj.Manual):
|
|
16
|
+
definition = '''
|
|
17
|
+
-> Procedure
|
|
18
|
+
-> Probe
|
|
19
|
+
---
|
|
20
|
+
insertion_ap : float # anterior posterior distance from bregma
|
|
21
|
+
insertion_ml : float # median lateral distance from bregma
|
|
22
|
+
insertion_depth : float # insertion depth (how much shank is inserted from dura)
|
|
23
|
+
insertion_el : float # elevation of the probe (angle)
|
|
24
|
+
insertion_az : float # azimuth of the probe (angle)
|
|
25
|
+
insertion_spin = 0 : float # spin on the probe shanks
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
@dataschema
|
|
29
|
+
class ProbeExtraction(dj.Manual):
|
|
30
|
+
definition = '''
|
|
31
|
+
-> Procedure
|
|
32
|
+
-> Probe
|
|
33
|
+
---
|
|
34
|
+
extraction_successful : tinyint # boolean for successfull or not
|
|
35
|
+
'''
|
|
36
|
+
|
|
37
|
+
@dataschema
|
|
38
|
+
class ProbeConfiguration(dj.Manual):
|
|
39
|
+
definition = '''
|
|
40
|
+
-> Probe
|
|
41
|
+
configuration_id : smallint
|
|
42
|
+
---
|
|
43
|
+
probe_n_channels : int # number of connected channels
|
|
44
|
+
probe_gain : float # gain of the probe (multiplication factor)
|
|
45
|
+
channel_idx : blob # index of the channels
|
|
46
|
+
channel_shank : blob # shank of each channel
|
|
47
|
+
channel_coords : blob # channel x,y position
|
|
48
|
+
'''
|
|
49
|
+
def add_from_spikeglx_metadata(self,metadata):
|
|
50
|
+
'''
|
|
51
|
+
Metadata can be a dictionary (with the metadata) or the path to an ap.meta file.
|
|
52
|
+
'''
|
|
53
|
+
from ..rules.ephys import get_probe_configuration
|
|
54
|
+
if not hasattr(metadata,'keys'):
|
|
55
|
+
conf = get_probe_configuration(metadata)
|
|
56
|
+
else:
|
|
57
|
+
conf = metadata
|
|
58
|
+
|
|
59
|
+
probeid = conf['probe_id']
|
|
60
|
+
if not len(Probe() & f'probe_id = "{probeid}"'):
|
|
61
|
+
Probe.insert1(dict(probe_id = conf['probe_id'],
|
|
62
|
+
probe_type = conf['probe_type'],
|
|
63
|
+
probe_n_shanks = conf['probe_n_shanks'],
|
|
64
|
+
probe_recording_channels = conf['probe_recording_channels']))
|
|
65
|
+
configs = (ProbeConfiguration() & f'probe_id = "{probeid}"').fetch(as_dict = True)
|
|
66
|
+
for c in configs:
|
|
67
|
+
if ((c['channel_coords'] == conf['channel_coords']).all() and
|
|
68
|
+
(c['channel_idx'] == conf['channel_idx']).all()):
|
|
69
|
+
print("The coords are the same, probe is already there. ")
|
|
70
|
+
|
|
71
|
+
return dict(probe_id = probeid,
|
|
72
|
+
configuration_id = c['configuration_id'],
|
|
73
|
+
sampling_rate = conf['sampling_rate'],
|
|
74
|
+
recording_software = conf['recording_software'],
|
|
75
|
+
recording_duration = conf['recording_duration'])
|
|
76
|
+
# add to configuration
|
|
77
|
+
confid = len(configs)+1
|
|
78
|
+
ProbeConfiguration.insert1(dict(probe_id = probeid,
|
|
79
|
+
configuration_id = confid,
|
|
80
|
+
probe_n_channels = conf['probe_n_channels'],
|
|
81
|
+
probe_gain = conf['probe_gain'],
|
|
82
|
+
channel_idx = conf['channel_idx'],
|
|
83
|
+
channel_shank = conf['channel_shank'],
|
|
84
|
+
channel_coords = conf['channel_coords']))
|
|
85
|
+
return dict(probe_id = probeid,
|
|
86
|
+
configuration_id = confid,
|
|
87
|
+
sampling_rate = conf['sampling_rate'],
|
|
88
|
+
recording_software = conf['recording_software'],
|
|
89
|
+
recording_duration = conf['recording_duration'])
|
|
90
|
+
|
|
91
|
+
@dataschema
|
|
92
|
+
class EphysRecording(dj.Imported):
|
|
93
|
+
definition = '''
|
|
94
|
+
-> Dataset
|
|
95
|
+
---
|
|
96
|
+
n_probes : smallint # number of probes
|
|
97
|
+
recording_duration : float # duration of the recording
|
|
98
|
+
recording_software : varchar(56) # software_version
|
|
99
|
+
'''
|
|
100
|
+
|
|
101
|
+
class ProbeSetting(dj.Part):
|
|
102
|
+
definition = '''
|
|
103
|
+
-> master
|
|
104
|
+
probe_num : smallint # probe number
|
|
105
|
+
---
|
|
106
|
+
-> ProbeConfiguration
|
|
107
|
+
sampling_rate : decimal(22,14) # sampling rate
|
|
108
|
+
'''
|
|
109
|
+
class ProbeFile(dj.Part):
|
|
110
|
+
definition = '''
|
|
111
|
+
-> EphysRecording.ProbeSetting
|
|
112
|
+
-> File # binary file that contains the data
|
|
113
|
+
'''
|
|
114
|
+
|
|
115
|
+
def add_spikeglx_recording(self,key):
|
|
116
|
+
'''
|
|
117
|
+
Adds a recording from Dataset ap.meta files.
|
|
118
|
+
'''
|
|
119
|
+
allpaths = pd.DataFrame((Dataset.DataFiles() & key).fetch()).file_path.values
|
|
120
|
+
paths = natsorted(list(filter( lambda x: x.endswith('.ap.meta'),
|
|
121
|
+
allpaths)))
|
|
122
|
+
keys = []
|
|
123
|
+
local_path = Path(prefs['local_paths'][0])
|
|
124
|
+
for iprobe, p in enumerate(paths):
|
|
125
|
+
# add each configuration
|
|
126
|
+
tmp = ProbeConfiguration().add_from_spikeglx_metadata(local_path/p)
|
|
127
|
+
tt = dict(key,n_probes = len(paths),probe_num = iprobe,**tmp)
|
|
128
|
+
EphysRecording.insert1(tt,
|
|
129
|
+
ignore_extra_fields = True,
|
|
130
|
+
skip_duplicates = True,
|
|
131
|
+
allow_direct_insert = True)
|
|
132
|
+
EphysRecording.ProbeSetting.insert1(tt,
|
|
133
|
+
ignore_extra_fields = True,
|
|
134
|
+
skip_duplicates = True,
|
|
135
|
+
allow_direct_insert = True)
|
|
136
|
+
# only working for spikeglx files for the moment.
|
|
137
|
+
pfiles = list(filter(lambda x: f'imec{iprobe}.ap.' in x,allpaths))
|
|
138
|
+
EphysRecording.ProbeFile().insert([
|
|
139
|
+
dict(tt,
|
|
140
|
+
**(File() & f'file_path = "{fi}"').proj().fetch(as_dict = True)[0])
|
|
141
|
+
for fi in pfiles],
|
|
142
|
+
skip_duplicates = True,
|
|
143
|
+
ignore_extra_fields = True,
|
|
144
|
+
allow_direct_insert = True)
|
|
145
|
+
EphysRecordingNoiseStats().populate(tt) # try to populate the NoiseStats table (this will take a couple of minutes)
|
|
146
|
+
|
|
147
|
+
def add_nidq_events(self,key = None):
|
|
148
|
+
if key is None:
|
|
149
|
+
key = [k for k in self] # create a list
|
|
150
|
+
if type(key) is dict:
|
|
151
|
+
key = [key]
|
|
152
|
+
for k in key:
|
|
153
|
+
dkey = (Dataset() & key).proj().fetch1()
|
|
154
|
+
dkey = dict(dkey,
|
|
155
|
+
stream_name = 'nidq')
|
|
156
|
+
|
|
157
|
+
if len(DatasetEvents() & dkey):
|
|
158
|
+
print(f' DatasetEvents for nidq are already there for {dkey}')
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
allpaths = pd.DataFrame((Dataset.DataFiles() & k).fetch()).file_path.values
|
|
162
|
+
paths = list(filter( lambda x: '.nidq.' in x, allpaths))
|
|
163
|
+
file_paths = (File() & [dict(file_path = p) for p in paths])
|
|
164
|
+
try:
|
|
165
|
+
file_paths = file_paths.get() # download or get the path
|
|
166
|
+
except ValueError:
|
|
167
|
+
raise(ValueError(f'Error getting files for dataset {k}.'))
|
|
168
|
+
from ..rules.ephys import extract_events_from_nidq
|
|
169
|
+
events,daq = extract_events_from_nidq(file_paths)
|
|
170
|
+
|
|
171
|
+
if not len(events):
|
|
172
|
+
print(f'No events for key: {k}')
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
DatasetEvents().insert1(dkey, allow_direct_insert = True)
|
|
176
|
+
DatasetEvents.Digital().insert([dict(dkey,**ev) for ev in events], allow_direct_insert = True)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
@dataschema
|
|
180
|
+
class EphysRecordingNoiseStats(dj.Computed):
|
|
181
|
+
# Statistics to access recording noise on multisite
|
|
182
|
+
definition = '''
|
|
183
|
+
-> EphysRecording.ProbeSetting
|
|
184
|
+
---
|
|
185
|
+
channel_median = NULL : longblob # nchannels*2 array, the 1st column is the start, 2nd at the end of the file
|
|
186
|
+
channel_max = NULL : longblob
|
|
187
|
+
channel_min = NULL : longblob
|
|
188
|
+
channel_peak_to_peak = NULL : longblob
|
|
189
|
+
channel_mad = NULL : longblob # median absolute deviation
|
|
190
|
+
'''
|
|
191
|
+
duration = 30 # duration of the stretch to sample (takes it from the start and the end of the file)
|
|
192
|
+
|
|
193
|
+
def make(self,key):
|
|
194
|
+
files = pd.DataFrame((EphysRecording.ProbeFile() & key).fetch())
|
|
195
|
+
assert len(files), ValueError(f'No files for dataset {key}')
|
|
196
|
+
# search for the recording files (this is set for compressed files now)
|
|
197
|
+
recording_file = list(filter(lambda x : 'ap.cbin' in x,files.file_path.values))
|
|
198
|
+
assert len(recording_file),ValueError(f'Could not find ap.cbin for {key}. Check Dataset.DataFiles?')
|
|
199
|
+
recording_file = recording_file[0]
|
|
200
|
+
filepath = find_local_filepath(recording_file, allowed_extensions = ['.ap.bin'])
|
|
201
|
+
assert not filepath is None, ValueError(f'File [{recording_file}] not found in local paths.')
|
|
202
|
+
# to get the gain, the channel_indices, and the sampling rate
|
|
203
|
+
config = pd.DataFrame((ProbeConfiguration()*EphysRecording.ProbeSetting() & key).fetch()).iloc[0]
|
|
204
|
+
# compute
|
|
205
|
+
from ..rules.ephys import ephys_noise_statistics_from_file
|
|
206
|
+
noisestats = ephys_noise_statistics_from_file(filepath,
|
|
207
|
+
duration = self.duration,
|
|
208
|
+
channel_indices = config.channel_idx,
|
|
209
|
+
sampling_rate = float(config.sampling_rate),
|
|
210
|
+
gain = config.probe_gain)
|
|
211
|
+
self.insert1(dict(key,**noisestats),ignore_extra_fields = True)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataschema
|
|
215
|
+
class SpikeSortingParams(dj.Manual):
|
|
216
|
+
definition = '''
|
|
217
|
+
parameter_set_num : int # number of the parameters set
|
|
218
|
+
---
|
|
219
|
+
algorithm_name : varchar(64) # preprocessing and spike sorting algorithm
|
|
220
|
+
parameters_dict : varchar(2000) # parameters json formatted dictionary
|
|
221
|
+
code_link = NULL : varchar(300) # the software that preprocesses and sorts
|
|
222
|
+
'''
|
|
223
|
+
|
|
224
|
+
@dataschema
|
|
225
|
+
class SpikeSorting(dj.Manual):
|
|
226
|
+
definition = '''
|
|
227
|
+
-> EphysRecording.ProbeSetting
|
|
228
|
+
-> SpikeSortingParams
|
|
229
|
+
---
|
|
230
|
+
n_pre_samples : smallint # to compute the waveform time
|
|
231
|
+
n_sorted_units = NULL : int # number of sorted units
|
|
232
|
+
n_detected_spikes = NULL : int # number of detected spikes
|
|
233
|
+
sorting_datetime = NULL : datetime # date of the spike sorting analysis
|
|
234
|
+
channel_indices = NULL : longblob # channel_map
|
|
235
|
+
channel_coords = NULL : longblob # channel_positions
|
|
236
|
+
-> [nullable] AnalysisFile.proj(features_file='file_path',features_storage='storage')
|
|
237
|
+
-> [nullable] AnalysisFile.proj(waveforms_file='file_path',waveforms_storage='storage')
|
|
238
|
+
'''
|
|
239
|
+
# For each sorting, create a "features.hdf5" file that has the: (this file can be > 4Gb)
|
|
240
|
+
# - template features
|
|
241
|
+
# - cluster indices
|
|
242
|
+
# - whitening_matrix
|
|
243
|
+
# - templates
|
|
244
|
+
# For each sorting create a "waveforms.hdf5" file that has the: (this file can be > 10Gb)
|
|
245
|
+
# - filtered waveform samples for each unit (1000 per unit)
|
|
246
|
+
# - indices of the extracted waveforms
|
|
247
|
+
|
|
248
|
+
class Segment(dj.Part):
|
|
249
|
+
definition = '''
|
|
250
|
+
-> master
|
|
251
|
+
segment_num : int # number of the segment
|
|
252
|
+
---
|
|
253
|
+
offset_samples : int # offset where the traces comes from
|
|
254
|
+
segment : longblob # 2 second segment of data in the AP band
|
|
255
|
+
'''
|
|
256
|
+
|
|
257
|
+
class Unit(dj.Part):
|
|
258
|
+
definition = '''
|
|
259
|
+
-> master
|
|
260
|
+
unit_id : int # cluster id
|
|
261
|
+
---
|
|
262
|
+
spike_times : longblob # in samples (uint64)
|
|
263
|
+
spike_positions = NULL : longblob # spike position in the electrode (float32)
|
|
264
|
+
spike_amplitudes = NULL : longblob # spike template amplitudes (float32)
|
|
265
|
+
'''
|
|
266
|
+
def get_sampling_rates(self):
|
|
267
|
+
sampling_rates = (self*EphysRecording.ProbeSetting()).fetch('sampling_rate')
|
|
268
|
+
return [float(s) for s in sampling_rates] # cast to float if decimal
|
|
269
|
+
|
|
270
|
+
def get_units_with_waveforms(self, return_seconds = True):
|
|
271
|
+
units = []
|
|
272
|
+
sampling_rates = self.get_sampling_rates()
|
|
273
|
+
# get the interpolation functions for all experiments if return_seconds = True
|
|
274
|
+
if return_seconds:
|
|
275
|
+
exps = (EphysRecording.ProbeSetting() & self.proj()).proj().fetch(as_dict = True)
|
|
276
|
+
interpolations = dict()
|
|
277
|
+
for e in exps:
|
|
278
|
+
k = '{subject_name}_{session_name}_{dataset_name}_{probe_num}'.format(**e)
|
|
279
|
+
try:
|
|
280
|
+
interpolations[k] = (StreamSync() & e).apply(None)
|
|
281
|
+
except:
|
|
282
|
+
interpolations[k] = None
|
|
283
|
+
for p,u,r in zip(self.proj(),self,sampling_rates):
|
|
284
|
+
w = (SpikeSorting.Waveforms() & p)
|
|
285
|
+
if len(w):
|
|
286
|
+
w = w.fetch('waveform_median')[0]
|
|
287
|
+
else:
|
|
288
|
+
w = None
|
|
289
|
+
units.append(dict(u, waveform_median = w))
|
|
290
|
+
if return_seconds:
|
|
291
|
+
k = '{subject_name}_{session_name}_{dataset_name}_{probe_num}'.format(**p)
|
|
292
|
+
if interpolations[k] is None:
|
|
293
|
+
units[-1]['spike_times'] = units[-1]['spike_times'].astype(np.float32)/np.float32(r)
|
|
294
|
+
else:
|
|
295
|
+
units[-1]['spike_times'] = interpolations[k](units[-1]['spike_times'].astype(np.float32))
|
|
296
|
+
return units
|
|
297
|
+
|
|
298
|
+
def get_spike_times(self, as_dict = True, return_seconds = True, extra_keys = [], warn = False, include_metrics = False):
|
|
299
|
+
'''
|
|
300
|
+
spike_times = get_spike_times()
|
|
301
|
+
|
|
302
|
+
Gets spike times corrected if the sync is applied.
|
|
303
|
+
|
|
304
|
+
as_dict = True
|
|
305
|
+
return_seconds = True
|
|
306
|
+
extra_keys = []
|
|
307
|
+
warn = False
|
|
308
|
+
include_metrics = False
|
|
309
|
+
'''
|
|
310
|
+
if return_seconds:
|
|
311
|
+
exps = (EphysRecording.ProbeSetting() & self.proj()).proj().fetch(as_dict = True)
|
|
312
|
+
interpolations = dict()
|
|
313
|
+
for e in exps:
|
|
314
|
+
k = '{subject_name}_{session_name}_{dataset_name}_{probe_num}'.format(**e)
|
|
315
|
+
try:
|
|
316
|
+
probe_num = e['probe_num']
|
|
317
|
+
interpolations[k] = (StreamSync() & e & f'stream_name = "imec{probe_num}"').apply(None, warn = False)
|
|
318
|
+
except AssertionError as err:
|
|
319
|
+
import warnings
|
|
320
|
+
warnings.warn(f"Using the sampling rate for spike times", RuntimeWarning)
|
|
321
|
+
interpolations[k] = lambda x: x/float((EphysRecording.ProbeSetting() & e).fetch1('sampling_rate'))
|
|
322
|
+
keys = ['subject_name','session_name','dataset_name','probe_num','parameter_set_num','unit_id','spike_times'] + extra_keys
|
|
323
|
+
|
|
324
|
+
if include_metrics:
|
|
325
|
+
units = (self*UnitMetrics).fetch(*keys,as_dict=True)
|
|
326
|
+
else:
|
|
327
|
+
units = self.fetch(*keys,as_dict=True)
|
|
328
|
+
if return_seconds:
|
|
329
|
+
for u in units:
|
|
330
|
+
k = '{subject_name}_{session_name}_{dataset_name}_{probe_num}'.format(**u)
|
|
331
|
+
u['spike_times'] = interpolations[k](u['spike_times'])
|
|
332
|
+
if as_dict:
|
|
333
|
+
return units
|
|
334
|
+
return [u['spike_times'] for u in units]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class Waveforms(dj.Part):
|
|
338
|
+
definition = '''
|
|
339
|
+
-> SpikeSorting.Unit
|
|
340
|
+
---
|
|
341
|
+
waveform_median : longblob # average waveform (gain corrected in microvolt - float32)
|
|
342
|
+
'''
|
|
343
|
+
|
|
344
|
+
def delete(
|
|
345
|
+
self,
|
|
346
|
+
transaction = True,
|
|
347
|
+
safemode = None,
|
|
348
|
+
force_parts = False):
|
|
349
|
+
|
|
350
|
+
files = [f['waveforms_file'] for f in self]
|
|
351
|
+
files += [f['features_file'] for f in self]
|
|
352
|
+
super().delete(transaction = transaction,
|
|
353
|
+
safemode = safemode,
|
|
354
|
+
force_parts = force_parts)
|
|
355
|
+
if not len(self) == 0:
|
|
356
|
+
# delete analysis files
|
|
357
|
+
for t in files:
|
|
358
|
+
(AnalysisFile() & f'file_path = "{t}"').delete(force_parts=force_parts, safemode = safemode)
|
|
359
|
+
|
|
360
|
+
def delete_sorting(
|
|
361
|
+
self,
|
|
362
|
+
transaction = True,
|
|
363
|
+
safemode = None,
|
|
364
|
+
force_parts = False):
|
|
365
|
+
files = [f['waveforms_file'] for f in self]
|
|
366
|
+
files += [f['features_file'] for f in self]
|
|
367
|
+
if not len(files) == 0:
|
|
368
|
+
# delete analysis files
|
|
369
|
+
for t in files:
|
|
370
|
+
(AnalysisFile() & f'file_path = "{t}"').delete(transaction = transaction,
|
|
371
|
+
force_parts = force_parts,
|
|
372
|
+
safemode = safemode)
|
|
373
|
+
|
|
374
|
+
@analysisschema
|
|
375
|
+
class UnitMetrics(dj.Computed):
|
|
376
|
+
default_container = 'labdata-spks'
|
|
377
|
+
# Compute the metrics from the each unit,
|
|
378
|
+
# so we can recompute and add new ones if needed and not depend on the clustering
|
|
379
|
+
definition = '''
|
|
380
|
+
-> SpikeSorting.Unit
|
|
381
|
+
---
|
|
382
|
+
num_spikes : int
|
|
383
|
+
depth = NULL : double
|
|
384
|
+
position = NULL : blob
|
|
385
|
+
shank = NULL : int
|
|
386
|
+
channel_index = NULL : int
|
|
387
|
+
n_electrodes_spanned = NULL : int
|
|
388
|
+
firing_rate = NULL : float
|
|
389
|
+
isi_contamination = NULL : float
|
|
390
|
+
isi_contamination_hill = NULL : float
|
|
391
|
+
amplitude_cutoff = NULL : float
|
|
392
|
+
presence_ratio = NULL : float
|
|
393
|
+
depth_drift_range = NULL : float
|
|
394
|
+
depth_drift_fluctuation = NULL : float
|
|
395
|
+
spike_amplitude = NULL : float
|
|
396
|
+
spike_duration = NULL : float
|
|
397
|
+
trough_time = NULL : float
|
|
398
|
+
trough_amplitude = NULL : float
|
|
399
|
+
fw3m = NULL : float
|
|
400
|
+
trough_gradient = NULL : float
|
|
401
|
+
peak_gradient = NULL : float
|
|
402
|
+
peak_time = NULL : float
|
|
403
|
+
peak_amplitude = NULL : float
|
|
404
|
+
polarity = NULL : tinyint
|
|
405
|
+
active_electrodes = NULL : blob
|
|
406
|
+
'''
|
|
407
|
+
def make(self, key):
|
|
408
|
+
dat = (SpikeSorting.Unit & key).get_units_with_waveforms()
|
|
409
|
+
assert len(dat) == 1, ValueError('Need to select only one unit')
|
|
410
|
+
dat = dat[0]
|
|
411
|
+
|
|
412
|
+
from spks.metrics import (isi_contamination,
|
|
413
|
+
isi_contamination_hill,
|
|
414
|
+
amplitude_cutoff,
|
|
415
|
+
presence_ratio,
|
|
416
|
+
firing_rate,
|
|
417
|
+
depth_stability)
|
|
418
|
+
from spks.waveforms import waveforms_position, compute_waveform_metrics
|
|
419
|
+
|
|
420
|
+
kk = {k:dat[k] for k in ['subject_name','session_name','dataset_name','probe_num']}
|
|
421
|
+
channel_coords,srate,wpre = (EphysRecording.ProbeSetting()*SpikeSorting() & kk
|
|
422
|
+
& f'parameter_set_num = {key["parameter_set_num"]}').fetch1(
|
|
423
|
+
'channel_coords','sampling_rate','n_pre_samples')
|
|
424
|
+
channel_shanks,duration = (EphysRecording()*EphysRecording.ProbeSetting()*
|
|
425
|
+
ProbeConfiguration() & kk).fetch1('channel_shank','recording_duration')
|
|
426
|
+
|
|
427
|
+
metrics = dict(key)
|
|
428
|
+
|
|
429
|
+
metrics['num_spikes'] = len(dat['spike_times'])
|
|
430
|
+
if metrics['num_spikes'] > 5: # skip if less than 5 spikes
|
|
431
|
+
metrics['firing_rate'] = firing_rate(dat['spike_times'],0, t_max = duration)
|
|
432
|
+
if metrics['num_spikes'] > 50: # skip if less than 50 spikes
|
|
433
|
+
metrics['isi_contamination'] = isi_contamination(dat['spike_times'], T = duration)
|
|
434
|
+
metrics['isi_contamination_hill'] = isi_contamination_hill(dat['spike_times'],
|
|
435
|
+
T = duration)
|
|
436
|
+
metrics['amplitude_cutoff'] = amplitude_cutoff(dat['spike_amplitudes'])
|
|
437
|
+
metrics['presence_ratio'] = presence_ratio(dat['spike_times'],
|
|
438
|
+
t_min = 0, t_max = duration)
|
|
439
|
+
metrics['depth_drift_range'],metrics['depth_drift_fluctuation'] = depth_stability(
|
|
440
|
+
dat['spike_times'],
|
|
441
|
+
dat['spike_positions'][:,1],tmax = duration)
|
|
442
|
+
if not dat['waveform_median'] is None:
|
|
443
|
+
waves = dat['waveform_median']
|
|
444
|
+
|
|
445
|
+
pos,channel,active_idx = waveforms_position(np.expand_dims(dat['waveform_median'],
|
|
446
|
+
axis = 0),
|
|
447
|
+
channel_positions = channel_coords,
|
|
448
|
+
active_electrode_threshold = 3)
|
|
449
|
+
if not np.all(np.isfinite(pos)):
|
|
450
|
+
# this can happen when there is noise in the waveform estimate, choose the position of the peak channel then
|
|
451
|
+
pos = channel_coords[channel]
|
|
452
|
+
active_idx = [channel]
|
|
453
|
+
metrics['n_electrodes_spanned'] = len(active_idx[0])
|
|
454
|
+
if len(active_idx):
|
|
455
|
+
metrics['active_electrodes'] = np.array(active_idx).astype(int)
|
|
456
|
+
metrics['depth'] = pos[0][1] # TODO: estimate position from 6 channels around the peak channel
|
|
457
|
+
metrics['position'] = pos[0]
|
|
458
|
+
metrics['shank'] = channel_shanks[channel[0]]
|
|
459
|
+
metrics['channel_index'] = channel[0]
|
|
460
|
+
# the com only works if it is done only for the values that have spikes
|
|
461
|
+
wavemetrics = compute_waveform_metrics(waves[:,channel[0]],
|
|
462
|
+
wpre, float(srate))
|
|
463
|
+
metrics = dict(metrics,**wavemetrics)
|
|
464
|
+
metrics['spike_amplitude'] = np.abs(metrics['trough_amplitude']-metrics['peak_amplitude'])
|
|
465
|
+
self.insert1(metrics,skip_duplicates = True)
|
|
466
|
+
|
|
467
|
+
@analysisschema
|
|
468
|
+
class UnitCountCriteria(dj.Manual):
|
|
469
|
+
definition = '''
|
|
470
|
+
unit_criteria_id : int
|
|
471
|
+
---
|
|
472
|
+
sua_criteria : varchar(2000)
|
|
473
|
+
mua_criteria = NULL : varchar(2000) # if NULL, subtracts the SUA labels.
|
|
474
|
+
'''
|
|
475
|
+
|
|
476
|
+
@analysisschema
|
|
477
|
+
class UnitCount(dj.Computed):
|
|
478
|
+
definition = '''
|
|
479
|
+
-> SpikeSorting
|
|
480
|
+
-> UnitCountCriteria
|
|
481
|
+
---
|
|
482
|
+
all : int
|
|
483
|
+
sua : int
|
|
484
|
+
mua : int
|
|
485
|
+
'''
|
|
486
|
+
class Unit(dj.Part):
|
|
487
|
+
definition = '''
|
|
488
|
+
-> master
|
|
489
|
+
-> UnitMetrics
|
|
490
|
+
---
|
|
491
|
+
passes : tinyint
|
|
492
|
+
'''
|
|
493
|
+
|
|
494
|
+
def make(self,key):
|
|
495
|
+
allu = pd.DataFrame((UnitMetrics() & key).fetch())
|
|
496
|
+
criteria = (UnitCountCriteria() &
|
|
497
|
+
f"unit_criteria_id = {key['unit_criteria_id']}").fetch('sua_criteria')[0]
|
|
498
|
+
suaidx = _apply_unit_criteria(allu, criteria)
|
|
499
|
+
sua = np.sum(suaidx)
|
|
500
|
+
muacriteria = (UnitCountCriteria() &
|
|
501
|
+
f"unit_criteria_id = {key['unit_criteria_id']}").fetch('mua_criteria')[0]
|
|
502
|
+
if muacriteria is None:
|
|
503
|
+
mua = len(allu) - np.sum(suaidx)
|
|
504
|
+
else:
|
|
505
|
+
mua = np.sum(_apply_unit_criteria(allu, muacriteria))
|
|
506
|
+
# Code for parsing the unit count criteria missing here.
|
|
507
|
+
unitcounts = dict(key,
|
|
508
|
+
all = len(allu),
|
|
509
|
+
sua = sua,
|
|
510
|
+
mua = mua)
|
|
511
|
+
self.insert1(unitcounts)
|
|
512
|
+
# select only a projection later, for now we ignore the extra fields
|
|
513
|
+
allu['passes'] = suaidx.astype(int)
|
|
514
|
+
keys = [dict(a,unit_criteria_id = key['unit_criteria_id']) for i,a in allu.iterrows()]
|
|
515
|
+
self.Unit.insert(keys, ignore_extra_fields = True) # add to the Unit part table
|
|
516
|
+
|
|
517
|
+
def _apply_unit_criteria(unitmetrics,criteria):
|
|
518
|
+
parameters = [p.strip(' ') for p in criteria.split('&')]
|
|
519
|
+
operators = {'<': np.less, '>': np.greater, '<=':np.less_equal, '>=':np.greater_equal, '==': np.equal, '!=':np.not_equal}
|
|
520
|
+
idx = []
|
|
521
|
+
for p in parameters:
|
|
522
|
+
for o in operators.keys():
|
|
523
|
+
if o in p:
|
|
524
|
+
n = [t.strip(' ') for t in p.split(o)]
|
|
525
|
+
try:
|
|
526
|
+
n[1] = float(n[1])
|
|
527
|
+
except:
|
|
528
|
+
pass
|
|
529
|
+
idx.append(operators[o](unitmetrics[n[0]].values,n[1]))
|
|
530
|
+
for i in idx:
|
|
531
|
+
idx[0] = np.logical_and(idx[0],i)
|
|
532
|
+
return idx[0]
|
|
533
|
+
|
|
534
|
+
from .histology import Atlas
|
|
535
|
+
|
|
536
|
+
@analysisschema
|
|
537
|
+
class ProbeBrainRegion(dj.Manual):
|
|
538
|
+
definition = '''
|
|
539
|
+
-> ProbeInsertion
|
|
540
|
+
-> ProbeConfiguration
|
|
541
|
+
-> Atlas.Region
|
|
542
|
+
---
|
|
543
|
+
min_depth = NULL : double
|
|
544
|
+
max_depth = NULL : double
|
|
545
|
+
shanks = NULL : blob
|
|
546
|
+
electrodes = NULL : blob
|
|
547
|
+
'''
|