@@ -319,16 +319,6 @@ def insert_new_params(cls, processing_method: str, paramset_idx: int, paramset_d
319
319
cls .insert1 (param_dict )
320
320
321
321
322
- @schema
323
- class ClusteringTask (dj .Manual ):
324
- definition = """
325
- -> EphysRecording
326
- -> ClusteringParamSet
327
- ---
328
- clustering_output_dir: varchar(255) # clustering output directory relative to root data directory
329
- """
330
-
331
-
332
322
@schema
333
323
class ClusterQualityLabel (dj .Lookup ):
334
324
definition = """
@@ -345,39 +335,89 @@ class ClusterQualityLabel(dj.Lookup):
345
335
]
346
336
347
337
338
+ @schema
339
+ class ClusteringTask (dj .Manual ):
340
+ definition = """
341
+ -> EphysRecording
342
+ -> ClusteringParamSet
343
+ ---
344
+ clustering_output_dir: varchar(255) # clustering output directory relative to root data directory
345
+ task_mode='load': enum('load', 'trigger') # 'load': load computed analysis results, 'trigger': trigger computation
346
+ """
347
+
348
+
348
349
@schema
349
350
class Clustering (dj .Imported ):
351
+ """
352
+ A processing table to handle each ClusteringTask:
353
+ + If `task_mode == "trigger"`: trigger clustering analysis according to the ClusteringParamSet (e.g. launch a kilosort job)
354
+ + If `task_mode == "load"`: verify output and create a corresponding entry in the Curation table
355
+ """
350
356
definition = """
351
357
-> ClusteringTask
352
358
---
353
- clustering_time: datetime # time of generation of this set of clustering results
354
- quality_control: bool # has this clustering result undergone quality control?
355
- manual_curation: bool # has manual curation been performed on this clustering result?
356
- clustering_note='': varchar(2000)
359
+ clustering_time: datetime # time of generation of this set of clustering results
360
+ """
361
+
362
+ def make (self , key ):
363
+ root_dir = pathlib .Path (get_ephys_root_data_dir ())
364
+ task_mode , output_dir = (ClusteringTask & key ).fetch1 ('task_mode' , 'clustering_output_dir' )
365
+ ks_dir = root_dir / output_dir
366
+
367
+ if task_mode == 'load' :
368
+ ks = kilosort .Kilosort (ks_dir ) # check if the directory is a valid Kilosort output
369
+ creation_time , is_curated , is_qc = kilosort .extract_clustering_info (ks_dir )
370
+ # Synthesize curation_id
371
+ curation_id = (dj .U ().aggr (Curation & key , n = 'max(curation_id)' ).fetch1 ('n' ) or 0 ) + 1
372
+
373
+ self .insert1 ({** key , 'clustering_time' : creation_time })
374
+ Curation .insert1 ({** key , 'curation_id' : curation_id ,
375
+ 'curation_time' : creation_time , 'curation_output_dir' : output_dir ,
376
+ 'quality_control' : is_qc , 'manual_curation' : is_curated })
377
+ elif task_mode == 'trigger' :
378
+ raise NotImplementedError ('Automatic triggering of clustering analysis is not yet supported' )
379
+ else :
380
+ raise ValueError (f'Unknown task mode: { task_mode } ' )
381
+
382
+
383
+ @schema
384
+ class Curation (dj .Manual ):
385
+ definition = """
386
+ -> ClusteringTask
387
+ curation_id: int
388
+ ---
389
+ curation_time: datetime # time of generation of this set of curated clustering results
390
+ curation_output_dir: varchar(255) # output directory of the curated results, relative to root data directory
391
+ quality_control: bool # has this clustering result undergone quality control?
392
+ manual_curation: bool # has manual curation been performed on this clustering result?
393
+ curation_note='': varchar(2000)
357
394
"""
358
395
359
- class Unit (dj .Part ):
360
- definition = """
361
- -> master
362
- unit: int
363
- ---
364
- -> probe.ElectrodeConfig.Electrode # electrode on the probe that this unit has highest response amplitude
365
- -> ClusterQualityLabel
366
- spike_count: int # how many spikes in this recording of this unit
367
- spike_times: longblob # (s) spike times of this unit, relative to the start of the EphysRecording
368
- spike_sites : longblob # array of electrode associated with each spike
369
- spike_depths : longblob # (um) array of depths associated with each spike, relative to the (0, 0) of the probe
370
- """
396
+
397
+ @schema
398
+ class Unit (dj .Imported ):
399
+ definition = """
400
+ -> Curation
401
+ unit: int
402
+ ---
403
+ -> probe.ElectrodeConfig.Electrode # electrode on the probe that this unit has highest response amplitude
404
+ -> ClusterQualityLabel
405
+ spike_count: int # how many spikes in this recording of this unit
406
+ spike_times: longblob # (s) spike times of this unit, relative to the start of the EphysRecording
407
+ spike_sites : longblob # array of electrode associated with each spike
408
+ spike_depths : longblob # (um) array of depths associated with each spike, relative to the (0, 0) of the probe
409
+ """
410
+
411
+ @property
412
+ def key_source (self ):
413
+ return Curation ()
371
414
372
415
def make (self , key ):
373
416
root_dir = pathlib .Path (get_ephys_root_data_dir ())
374
- ks_dir = root_dir / (ClusteringTask & key ).fetch1 ('clustering_output_dir ' )
417
+ ks_dir = root_dir / (Curation & key ).fetch1 ('curation_output_dir ' )
375
418
ks = kilosort .Kilosort (ks_dir )
376
419
acq_software = (EphysRecording & key ).fetch1 ('acq_software' )
377
420
378
- # ---------- Clustering ----------
379
- creation_time , is_curated , is_qc = kilosort .extract_clustering_info (ks_dir )
380
-
381
421
# ---------- Unit ----------
382
422
# -- Remove 0-spike units
383
423
withspike_idx = [i for i , u in enumerate (ks .data ['cluster_ids' ]) if (ks .data ['spike_clusters' ] == u ).any ()]
@@ -413,15 +453,13 @@ def make(self, key):
413
453
'spike_sites' : spike_sites [ks .data ['spike_clusters' ] == unit ],
414
454
'spike_depths' : spike_depths [ks .data ['spike_clusters' ] == unit ]})
415
455
416
- self .insert1 ({** key , 'clustering_time' : creation_time ,
417
- 'quality_control' : is_qc , 'manual_curation' : is_curated })
418
- self .Unit .insert ([{** key , ** u } for u in units ])
456
+ self .insert ([{** key , ** u } for u in units ])
419
457
420
458
421
459
@schema
422
460
class Waveform (dj .Imported ):
423
461
definition = """
424
- -> Clustering. Unit
462
+ -> Unit
425
463
---
426
464
peak_chn_waveform_mean: longblob # mean over all spikes at the peak channel for this unit
427
465
"""
@@ -437,11 +475,11 @@ class Electrode(dj.Part):
437
475
438
476
@property
439
477
def key_source (self ):
440
- return Clustering ()
478
+ return Curation ()
441
479
442
480
def make (self , key ):
443
481
root_dir = pathlib .Path (get_ephys_root_data_dir ())
444
- ks_dir = root_dir / (ClusteringTask & key ).fetch1 ('clustering_output_dir ' )
482
+ ks_dir = root_dir / (Curation & key ).fetch1 ('curation_output_dir ' )
445
483
ks = kilosort .Kilosort (ks_dir )
446
484
447
485
acq_software , probe_sn = (EphysRecording * ProbeInsertion & key ).fetch1 ('acq_software' , 'probe' )
@@ -450,10 +488,10 @@ def make(self, key):
450
488
rec_key = (EphysRecording & key ).fetch1 ('KEY' )
451
489
chn2electrodes = get_neuropixels_chn2electrode_map (rec_key , acq_software )
452
490
453
- is_qc = (Clustering & key ).fetch1 ('quality_control' )
491
+ is_qc = (Curation & key ).fetch1 ('quality_control' )
454
492
455
493
# Get all units
456
- units = {u ['unit' ]: u for u in (Clustering . Unit & key ).fetch (as_dict = True , order_by = 'unit' )}
494
+ units = {u ['unit' ]: u for u in (Unit & key ).fetch (as_dict = True , order_by = 'unit' )}
457
495
458
496
unit_waveforms , unit_peak_waveforms = [], []
459
497
if is_qc :
@@ -494,7 +532,7 @@ def make(self, key):
494
532
@schema
495
533
class ClusterQualityMetrics (dj .Imported ):
496
534
definition = """
497
- -> Clustering. Unit
535
+ -> Unit
498
536
---
499
537
amp: float
500
538
snr: float
0 commit comments