1
+ import datajoint as dj
1
2
import pathlib
2
3
import re
3
4
import numpy as np
4
- import datajoint as dj
5
+ import inspect
5
6
import uuid
7
+ import hashlib
8
+ from collections .abc import Mapping
9
+
10
+ from .readers import neuropixels , kilosort
11
+ from . import probe
12
+
13
+ schema = dj .schema ()
14
+
15
+
16
+ def activate (ephys_schema_name , probe_schema_name = None , create_schema = True , create_tables = True , add_objects = None ):
17
+ upstream_tables = ("Session" , "SkullReference" )
18
+ assert isinstance (add_objects , Mapping )
19
+ try :
20
+ raise RuntimeError ("Table %s is required for module ephys" % next (
21
+ name for name in upstream_tables
22
+ if not isinstance (add_objects .get (name , None ), (dj .Manual , dj .Lookup , dj .Imported , dj .Computed ))))
23
+ except StopIteration :
24
+ pass # all ok
25
+
26
+ required_functions = ("get_neuropixels_data_directory" , "get_paramset_idx" , "get_kilosort_output_directory" )
27
+ assert isinstance (add_objects , Mapping )
28
+ try :
29
+ raise RuntimeError ("Function %s is required for module ephys" % next (
30
+ name for name in required_functions
31
+ if not inspect .isfunction (add_objects .get (name , None ))))
32
+ except StopIteration :
33
+ pass # all ok
34
+
35
+ if not probe .schema .is_activated :
36
+ probe .schema .activate (probe_schema_name or ephys_schema_name ,
37
+ create_schema = create_schema , create_tables = create_tables )
38
+ schema .activate (ephys_schema_name , create_schema = create_schema ,
39
+ create_tables = create_tables , add_objects = add_objects )
40
+
6
41
7
- from . import utils
8
- from .probe import schema , Probe , ProbeType , ElectrodeConfig
9
- from ephys_loaders import neuropixels , kilosort
42
+ # REQUIREMENTS: The workflow module must define these functions ---------------
10
43
11
- from djutils .templates import required
44
+
45
+ def get_neuropixels_data_directory ():
46
+ return None
47
+
48
+
49
+ def get_kilosort_output_directory (clustering_task_key : dict ) -> str :
50
+ """
51
+ Retrieve the Kilosort output directory for a given ClusteringTask
52
+ :param clustering_task_key: a dictionary of one EphysRecording
53
+ :return: a string for full path to the resulting Kilosort output directory
54
+ """
55
+ assert set (EphysRecording ().primary_key ) <= set (clustering_task_key )
56
+ raise NotImplementedError ('Workflow module should define' )
57
+
58
+
59
+ def get_paramset_idx (ephys_rec_key : dict ) -> int :
60
+ """
61
+ Retrieve attribute `paramset_idx` from the ClusteringParamSet record for the given EphysRecording key.
62
+ :param ephys_rec_key: a dictionary of one EphysRecording
63
+ :return: int specifying the `paramset_idx`
64
+ """
65
+ assert set (EphysRecording ().primary_key ) <= set (ephys_rec_key )
66
+ raise NotImplementedError ('Workflow module should define' )
67
+
68
+
69
+ def dict_to_uuid (key ):
70
+ """
71
+ Given a dictionary `key`, returns a hash string
72
+ """
73
+ hashed = hashlib .md5 ()
74
+ for k , v in sorted (key .items ()):
75
+ hashed .update (str (k ).encode ())
76
+ hashed .update (str (v ).encode ())
77
+ return uuid .UUID (hex = hashed .hexdigest ())
12
78
13
79
# ===================================== Probe Insertion =====================================
14
80
15
81
16
82
@schema
17
83
class ProbeInsertion (dj .Manual ): # (acute)
18
-
19
- _Session = ...
20
-
21
84
definition = """
22
- -> self._Session
85
+ -> Session
23
86
insertion_number: tinyint unsigned
24
87
---
25
- -> Probe
88
+ -> probe. Probe
26
89
"""
27
90
28
91
@@ -32,12 +95,10 @@ class ProbeInsertion(dj.Manual): # (acute)
32
95
@schema
33
96
class InsertionLocation (dj .Manual ):
34
97
35
- _SkullReference = ...
36
-
37
98
definition = """
38
99
-> ProbeInsertion
39
100
---
40
- -> self._SkullReference
101
+ -> SkullReference
41
102
ap_location: decimal(6, 2) # (um) anterior-posterior; ref is 0; more anterior is more positive
42
103
ml_location: decimal(6, 2) # (um) medial axis; ref is 0 ; more right is more positive
43
104
depth: decimal(6, 2) # (um) manipulator depth relative to surface of the brain (0); more ventral is more negative
@@ -48,7 +109,7 @@ class InsertionLocation(dj.Manual):
48
109
49
110
50
111
# ===================================== Ephys Recording =====================================
51
- # The abstract function _get_npx_data_dir () should expect one argument in the form of a
112
+ # The abstract function _get_neuropixels_data_directory () should expect one argument in the form of a
52
113
# dictionary with the keys from user-defined Subject and Session, as well as
53
114
# "insertion_number" (as int) based on the "ProbeInsertion" table definition in this djephys
54
115
@@ -62,33 +123,27 @@ class EphysRecording(dj.Imported):
62
123
sampling_rate: float # (Hz)
63
124
"""
64
125
65
- @staticmethod
66
- @required
67
- def _get_npx_data_dir ():
68
- return None
69
-
70
126
def make (self , key ):
71
- npx_dir = EphysRecording ._get_npx_data_dir (key )
72
-
73
- meta_filepath = next (pathlib .Path (npx_dir ).glob ('*.ap.meta' ))
127
+ neuropixels_dir = get_neuropixels_data_directory (key )
128
+ meta_filepath = next (pathlib .Path (neuropixels_dir ).glob ('*.ap.meta' ))
74
129
75
- npx_meta = neuropixels .NeuropixelsMeta (meta_filepath )
130
+ neuropixels_meta = neuropixels .NeuropixelsMeta (meta_filepath )
76
131
77
- if re .search ('(1.0|2.0)' , npx_meta .probe_model ):
132
+ if re .search ('(1.0|2.0)' , neuropixels_meta .probe_model ):
78
133
eg_members = []
79
- probe_type = {'probe_type' : npx_meta .probe_model }
80
- q_electrodes = ProbeType .Electrode & probe_type
81
- for shank , shank_col , shank_row , is_used in npx_meta .shankmap ['data' ]:
134
+ probe_type = {'probe_type' : neuropixels_meta .probe_model }
135
+ q_electrodes = probe . ProbeType .Electrode & probe_type
136
+ for shank , shank_col , shank_row , is_used in neuropixels_meta .shankmap ['data' ]:
82
137
electrode = (q_electrodes & {'shank' : shank ,
83
138
'shank_col' : shank_col ,
84
139
'shank_row' : shank_row }).fetch1 ('KEY' )
85
140
eg_members .append ({** electrode , 'used_in_reference' : is_used })
86
141
else :
87
142
raise NotImplementedError ('Processing for neuropixels probe model {} not yet implemented' .format (
88
- npx_meta .probe_model ))
143
+ neuropixels_meta .probe_model ))
89
144
90
145
# ---- compute hash for the electrode config (hash of dict of all ElectrodeConfig.Electrode) ----
91
- ec_hash = uuid .UUID (utils . dict_to_hash ({k ['electrode' ]: k for k in eg_members }))
146
+ ec_hash = uuid .UUID (dict_to_uuid ({k ['electrode' ]: k for k in eg_members }))
92
147
93
148
el_list = sorted ([k ['electrode' ] for k in eg_members ])
94
149
el_jumps = [- 1 ] + np .where (np .diff (el_list ) > 1 )[0 ].tolist () + [len (el_list ) - 1 ]
@@ -101,7 +156,7 @@ def make(self, key):
101
156
ElectrodeConfig .insert1 ({** e_config , ** probe_type , 'electrode_config_name' : ec_name })
102
157
ElectrodeConfig .Electrode .insert ({** e_config , ** m } for m in eg_members )
103
158
104
- self .insert1 ({** key , ** e_config , 'sampling_rate' : npx_meta .meta ['imSampRate' ]})
159
+ self .insert1 ({** key , ** e_config , 'sampling_rate' : neuropixels_meta .meta ['imSampRate' ]})
105
160
106
161
107
162
# ===========================================================================================
@@ -124,29 +179,29 @@ class LFP(dj.Imported):
124
179
class Electrode (dj .Part ):
125
180
definition = """
126
181
-> master
127
- -> ElectrodeConfig.Electrode
182
+ -> probe. ElectrodeConfig.Electrode
128
183
---
129
184
lfp: longblob # (mV) recorded lfp at this electrode
130
185
"""
131
186
132
187
def make (self , key ):
133
- npx_dir = EphysRecording ._get_npx_data_dir (key )
134
- npx_recording = neuropixels .Neuropixels (npx_dir )
188
+ neuropixels_dir = EphysRecording ._get_neuropixels_data_directory (key )
189
+ neuropixels_recording = neuropixels .Neuropixels (neuropixels_dir )
135
190
136
- lfp = npx_recording .lfdata [:, :- 1 ].T # exclude the sync channel
191
+ lfp = neuropixels_recording .lfdata [:, :- 1 ].T # exclude the sync channel
137
192
138
193
self .insert1 (dict (key ,
139
- lfp_sampling_rate = npx_recording .lfmeta ['imSampRate' ],
140
- lfp_time_stamps = np .arange (lfp .shape [1 ]) / npx_recording .lfmeta ['imSampRate' ],
194
+ lfp_sampling_rate = neuropixels_recording .lfmeta ['imSampRate' ],
195
+ lfp_time_stamps = np .arange (lfp .shape [1 ]) / neuropixels_recording .lfmeta ['imSampRate' ],
141
196
lfp_mean = lfp .mean (axis = 0 )))
142
197
'''
143
198
Only store LFP for every 9th channel (defined in skip_chn_counts), counting in reverse
144
199
Due to high channel density, close-by channels exhibit highly similar lfp
145
200
'''
146
- q_electrodes = ProbeType .Electrode * ElectrodeConfig .Electrode & key
201
+ q_electrodes = probe . ProbeType .Electrode * probe . ElectrodeConfig .Electrode & key
147
202
electrodes = []
148
203
for recorded_site in np .arange (lfp .shape [0 ]):
149
- shank , shank_col , shank_row , _ = npx_recording . npx_meta .shankmap ['data' ][recorded_site ]
204
+ shank , shank_col , shank_row , _ = neuropixels_recording . neuropixels_meta .shankmap ['data' ][recorded_site ]
150
205
electrodes .append ((q_electrodes
151
206
& {'shank' : shank ,
152
207
'shank_col' : shank_col ,
@@ -191,7 +246,7 @@ def insert_new_params(cls, processing_method: str, paramset_idx: int, paramset_d
191
246
'paramset_idx' : paramset_idx ,
192
247
'paramset_desc' : paramset_desc ,
193
248
'params' : params ,
194
- 'param_set_hash' : uuid . UUID ( utils . dict_to_hash ( params ) )}
249
+ 'param_set_hash' : dict_to_uuid ( params )}
195
250
q_param = cls & {'param_set_hash' : param_dict ['param_set_hash' ]}
196
251
197
252
if q_param : # If the specified param-set already exists
@@ -212,26 +267,6 @@ class ClusteringTask(dj.Imported):
212
267
-> ClusteringParamSet
213
268
"""
214
269
215
- @staticmethod
216
- @required
217
- def _get_paramset_idx (ephys_rec_key : dict ) -> int :
218
- """
219
- Retrieve the 'paramset_idx' (for ClusteringParamSet) to be used for this EphysRecording
220
- :param ephys_rec_key: a dictionary of one EphysRecording
221
- :return: int specifying the 'paramset_idx'
222
- """
223
- return None
224
-
225
- @staticmethod
226
- @required
227
- def _get_ks_data_dir (clustering_task_key : dict ) -> str :
228
- """
229
- Retrieve the Kilosort output directory for a given ClusteringTask
230
- :param clustering_task_key: a dictionary of one EphysRecording
231
- :return: a string for full path to the resulting Kilosort output directory
232
- """
233
- return None
234
-
235
270
def make (self , key ):
236
271
key ['paramset_idx' ] = ClusteringTask ._get_paramset_idx (key )
237
272
self .insert1 (key )
@@ -274,7 +309,7 @@ class Unit(dj.Part):
274
309
-> master
275
310
unit: int
276
311
---
277
- -> ElectrodeConfig.Electrode # electrode on the probe that this unit has highest response amplitude
312
+ -> probe. ElectrodeConfig.Electrode # electrode on the probe that this unit has highest response amplitude
278
313
-> ClusterQualityLabel
279
314
spike_count: int # how many spikes in this recording of this unit
280
315
spike_times: longblob # (s) spike times of this unit, relative to the start of the EphysRecording
@@ -294,7 +329,7 @@ def make(self, key):
294
329
valid_units = ks .data ['cluster_ids' ][withspike_idx ]
295
330
valid_unit_labels = ks .data ['cluster_groups' ][withspike_idx ]
296
331
# -- Get channel and electrode-site mapping
297
- chn2electrodes = get_npx_chn2electrode_map (key )
332
+ chn2electrodes = get_neuropixels_chn2electrode_map (key )
298
333
299
334
# -- Spike-times --
300
335
# spike_times_sec_adj > spike_times_sec > spike_times
@@ -339,7 +374,7 @@ class Waveform(dj.Imported):
339
374
class Electrode (dj .Part ):
340
375
definition = """
341
376
-> master
342
- -> ElectrodeConfig.Electrode
377
+ -> probe. ElectrodeConfig.Electrode
343
378
---
344
379
waveform_mean: longblob # mean over all spikes
345
380
waveforms=null: longblob # (spike x sample) waveform of each spike at each electrode
@@ -352,16 +387,16 @@ def key_source(self):
352
387
def make (self , key ):
353
388
units = {u ['unit' ]: u for u in (Clustering .Unit & key ).fetch (as_dict = True , order_by = 'unit' )}
354
389
355
- npx_dir = EphysRecording ._get_npx_data_dir (key )
356
- meta_filepath = next (pathlib .Path (npx_dir ).glob ('*.ap.meta' ))
357
- npx_meta = neuropixels .NeuropixelsMeta (meta_filepath )
390
+ neuropixels_dir = EphysRecording ._get_neuropixels_data_directory (key )
391
+ meta_filepath = next (pathlib .Path (neuropixels_dir ).glob ('*.ap.meta' ))
392
+ neuropixels_meta = neuropixels .NeuropixelsMeta (meta_filepath )
358
393
359
394
ks_dir = ClusteringTask ._get_ks_data_dir (key )
360
395
ks = kilosort .Kilosort (ks_dir )
361
396
362
397
# -- Get channel and electrode-site mapping
363
398
rec_key = (EphysRecording & key ).fetch1 ('KEY' )
364
- chn2electrodes = get_npx_chn2electrode_map (rec_key )
399
+ chn2electrodes = get_neuropixels_chn2electrode_map (rec_key )
365
400
366
401
is_qc = (Clustering & key ).fetch1 ('quality_control' )
367
402
@@ -375,10 +410,10 @@ def make(self, key):
375
410
if chn2electrodes [chn ]['electrode' ] == units [unit_no ]['electrode' ]:
376
411
unit_peak_waveforms .append ({** units [unit_no ], 'peak_chn_waveform_mean' : chn_wf })
377
412
else :
378
- npx_recording = neuropixels .Neuropixels (npx_dir )
413
+ neuropixels_recording = neuropixels .Neuropixels (neuropixels_dir )
379
414
for unit_no , unit_dict in units .items ():
380
415
spks = (Clustering .Unit & unit_dict ).fetch1 ('unit_spike_times' )
381
- wfs = npx_recording .extract_spike_waveforms (spks , ks .data ['channel_map' ]) # (sample x channel x spike)
416
+ wfs = neuropixels_recording .extract_spike_waveforms (spks , ks .data ['channel_map' ]) # (sample x channel x spike)
382
417
wfs = wfs .transpose ((1 , 2 , 0 )) # (channel x spike x sample)
383
418
for chn , chn_wf in zip (ks .data ['channel_map' ], wfs ):
384
419
unit_waveforms .append ({** unit_dict , ** chn2electrodes [chn ],
@@ -425,15 +460,15 @@ def make(self, key):
425
460
# ========================== HELPER FUNCTIONS =======================
426
461
427
462
428
- def get_npx_chn2electrode_map (ephys_recording_key ):
429
- npx_dir = EphysRecording ._get_npx_data_dir (ephys_recording_key )
430
- meta_filepath = next (pathlib .Path (npx_dir ).glob ('*.ap.meta' ))
431
- npx_meta = neuropixels .NeuropixelsMeta (meta_filepath )
432
- e_config_key = (EphysRecording * ElectrodeConfig & ephys_recording_key ).fetch1 ('KEY' )
463
+ def get_neuropixels_chn2electrode_map (ephys_recording_key ):
464
+ neuropixels_dir = EphysRecording ._get_neuropixels_data_directory (ephys_recording_key )
465
+ meta_filepath = next (pathlib .Path (neuropixels_dir ).glob ('*.ap.meta' ))
466
+ neuropixels_meta = neuropixels .NeuropixelsMeta (meta_filepath )
467
+ e_config_key = (EphysRecording * probe . ElectrodeConfig & ephys_recording_key ).fetch1 ('KEY' )
433
468
434
- q_electrodes = ProbeType .Electrode * ElectrodeConfig .Electrode & e_config_key
469
+ q_electrodes = probe . ProbeType .Electrode * probe . ElectrodeConfig .Electrode & e_config_key
435
470
chn2electrode_map = {}
436
- for recorded_site , (shank , shank_col , shank_row , _ ) in enumerate (npx_meta .shankmap ['data' ]):
471
+ for recorded_site , (shank , shank_col , shank_row , _ ) in enumerate (neuropixels_meta .shankmap ['data' ]):
437
472
chn2electrode_map [recorded_site ] = (q_electrodes
438
473
& {'shank' : shank ,
439
474
'shank_col' : shank_col ,
0 commit comments