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