@@ -328,16 +328,6 @@ def insert_new_params(cls, processing_method: str, paramset_idx: int, paramset_d
328
328
cls .insert1 (param_dict )
329
329
330
330
331
- @schema
332
- class ClusteringTask (dj .Manual ):
333
- definition = """
334
- -> EphysRecording
335
- -> ClusteringParamSet
336
- ---
337
- clustering_output_dir: varchar(255) # clustering output directory relative to root data directory
338
- """
339
-
340
-
341
331
@schema
342
332
class ClusterQualityLabel (dj .Lookup ):
343
333
definition = """
@@ -354,15 +344,82 @@ class ClusterQualityLabel(dj.Lookup):
354
344
]
355
345
356
346
347
+ @schema
348
+ class ClusteringTask (dj .Manual ):
349
+ definition = """
350
+ -> EphysRecording
351
+ -> ClusteringParamSet
352
+ ---
353
+ clustering_output_dir: varchar(255) # clustering output directory relative to root data directory
354
+ task_mode='load': enum('load', 'trigger') # 'load': load computed analysis results, 'trigger': trigger computation
355
+ """
356
+
357
+
357
358
@schema
358
359
class Clustering (dj .Imported ):
360
+ """
361
+ A processing table to handle each ClusteringTask:
362
+ + If `task_mode == "trigger"`: trigger clustering analysis according to the ClusteringParamSet (e.g. launch a kilosort job)
363
+ + If `task_mode == "load"`: verify output
364
+ """
359
365
definition = """
360
366
-> ClusteringTask
361
367
---
362
- clustering_time: datetime # time of generation of this set of clustering results
363
- quality_control: bool # has this clustering result undergone quality control?
364
- manual_curation: bool # has manual curation been performed on this clustering result?
365
- clustering_note='': varchar(2000)
368
+ clustering_time: datetime # time of generation of this set of clustering results
369
+ """
370
+
371
+ def make (self , key ):
372
+ root_dir = pathlib .Path (get_ephys_root_data_dir ())
373
+ task_mode , output_dir = (ClusteringTask & key ).fetch1 ('task_mode' , 'clustering_output_dir' )
374
+ ks_dir = root_dir / output_dir
375
+
376
+ if task_mode == 'load' :
377
+ ks = kilosort .Kilosort (ks_dir ) # check if the directory is a valid Kilosort output
378
+ creation_time , _ , _ = kilosort .extract_clustering_info (ks_dir )
379
+ elif task_mode == 'trigger' :
380
+ raise NotImplementedError ('Automatic triggering of clustering analysis is not yet supported' )
381
+ else :
382
+ raise ValueError (f'Unknown task mode: { task_mode } ' )
383
+
384
+ self .insert1 ({** key , 'clustering_time' : creation_time })
385
+
386
+
387
+ @schema
388
+ class Curation (dj .Manual ):
389
+ definition = """
390
+ -> Clustering
391
+ curation_id: int
392
+ ---
393
+ curation_time: datetime # time of generation of this set of curated clustering results
394
+ curation_output_dir: varchar(255) # output directory of the curated results, relative to root data directory
395
+ quality_control: bool # has this clustering result undergone quality control?
396
+ manual_curation: bool # has manual curation been performed on this clustering result?
397
+ curation_note='': varchar(2000)
398
+ """
399
+
400
+ def create1_from_clustering_task (self , key , curation_note = '' ):
401
+ """
402
+ A convenient function to create a new corresponding "Curation" for a particular "ClusteringTask"
403
+ """
404
+ if key not in Clustering ():
405
+ raise ValueError (f'No corresponding entry in Clustering available for: { key } ; do `Clustering.populate(key)`' )
406
+
407
+ root_dir = pathlib .Path (get_ephys_root_data_dir ())
408
+ task_mode , output_dir = (ClusteringTask & key ).fetch1 ('task_mode' , 'clustering_output_dir' )
409
+ ks_dir = root_dir / output_dir
410
+ creation_time , is_curated , is_qc = kilosort .extract_clustering_info (ks_dir )
411
+ # Synthesize curation_id
412
+ curation_id = dj .U ().aggr (self & key , n = 'ifnull(max(curation_id)+1,1)' ).fetch1 ('n' )
413
+ self .insert1 ({** key , 'curation_id' : curation_id ,
414
+ 'curation_time' : creation_time , 'curation_output_dir' : output_dir ,
415
+ 'quality_control' : is_qc , 'manual_curation' : is_curated ,
416
+ 'curation_note' : curation_note })
417
+
418
+
419
+ @schema
420
+ class CuratedClustering (dj .Imported ):
421
+ definition = """
422
+ -> Curation
366
423
"""
367
424
368
425
class Unit (dj .Part ):
@@ -380,13 +437,10 @@ class Unit(dj.Part):
380
437
381
438
def make (self , key ):
382
439
root_dir = pathlib .Path (get_ephys_root_data_dir ())
383
- ks_dir = root_dir / (ClusteringTask & key ).fetch1 ('clustering_output_dir ' )
440
+ ks_dir = root_dir / (Curation & key ).fetch1 ('curation_output_dir ' )
384
441
ks = kilosort .Kilosort (ks_dir )
385
442
acq_software = (EphysRecording & key ).fetch1 ('acq_software' )
386
443
387
- # ---------- Clustering ----------
388
- creation_time , is_curated , is_qc = kilosort .extract_clustering_info (ks_dir )
389
-
390
444
# ---------- Unit ----------
391
445
# -- Remove 0-spike units
392
446
withspike_idx = [i for i , u in enumerate (ks .data ['cluster_ids' ]) if (ks .data ['spike_clusters' ] == u ).any ()]
@@ -422,15 +476,14 @@ def make(self, key):
422
476
'spike_sites' : spike_sites [ks .data ['spike_clusters' ] == unit ],
423
477
'spike_depths' : spike_depths [ks .data ['spike_clusters' ] == unit ]})
424
478
425
- self .insert1 ({** key , 'clustering_time' : creation_time ,
426
- 'quality_control' : is_qc , 'manual_curation' : is_curated })
479
+ self .insert1 (key )
427
480
self .Unit .insert ([{** key , ** u } for u in units ])
428
481
429
482
430
483
@schema
431
484
class Waveform (dj .Imported ):
432
485
definition = """
433
- -> Clustering .Unit
486
+ -> CuratedClustering .Unit
434
487
---
435
488
peak_chn_waveform_mean: longblob # mean over all spikes at the peak channel for this unit
436
489
"""
@@ -446,11 +499,11 @@ class Electrode(dj.Part):
446
499
447
500
@property
448
501
def key_source (self ):
449
- return Clustering ()
502
+ return Curation ()
450
503
451
504
def make (self , key ):
452
505
root_dir = pathlib .Path (get_ephys_root_data_dir ())
453
- ks_dir = root_dir / (ClusteringTask & key ).fetch1 ('clustering_output_dir ' )
506
+ ks_dir = root_dir / (Curation & key ).fetch1 ('curation_output_dir ' )
454
507
ks = kilosort .Kilosort (ks_dir )
455
508
456
509
acq_software , probe_sn = (EphysRecording * ProbeInsertion & key ).fetch1 ('acq_software' , 'probe' )
@@ -459,10 +512,10 @@ def make(self, key):
459
512
rec_key = (EphysRecording & key ).fetch1 ('KEY' )
460
513
chn2electrodes = get_neuropixels_chn2electrode_map (rec_key , acq_software )
461
514
462
- is_qc = (Clustering & key ).fetch1 ('quality_control' )
515
+ is_qc = (Curation & key ).fetch1 ('quality_control' )
463
516
464
517
# Get all units
465
- units = {u ['unit' ]: u for u in (Clustering .Unit & key ).fetch (as_dict = True , order_by = 'unit' )}
518
+ units = {u ['unit' ]: u for u in (CuratedClustering .Unit & key ).fetch (as_dict = True , order_by = 'unit' )}
466
519
467
520
unit_waveforms , unit_peak_waveforms = [], []
468
521
if is_qc :
@@ -503,7 +556,7 @@ def make(self, key):
503
556
@schema
504
557
class ClusterQualityMetrics (dj .Imported ):
505
558
definition = """
506
- -> Clustering .Unit
559
+ -> CuratedClustering .Unit
507
560
---
508
561
amp: float
509
562
snr: float
0 commit comments