@@ -328,16 +328,6 @@ def insert_new_params(cls, processing_method: str, paramset_idx: int, paramset_d
328328 cls .insert1 (param_dict )
329329
330330
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-
341331@schema
342332class ClusterQualityLabel (dj .Lookup ):
343333 definition = """
@@ -354,15 +344,82 @@ class ClusterQualityLabel(dj.Lookup):
354344 ]
355345
356346
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+
357358@schema
358359class 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+ """
359365 definition = """
360366 -> ClusteringTask
361367 ---
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
366423 """
367424
368425 class Unit (dj .Part ):
@@ -380,13 +437,10 @@ class Unit(dj.Part):
380437
381438 def make (self , key ):
382439 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 ' )
384441 ks = kilosort .Kilosort (ks_dir )
385442 acq_software = (EphysRecording & key ).fetch1 ('acq_software' )
386443
387- # ---------- Clustering ----------
388- creation_time , is_curated , is_qc = kilosort .extract_clustering_info (ks_dir )
389-
390444 # ---------- Unit ----------
391445 # -- Remove 0-spike units
392446 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):
422476 'spike_sites' : spike_sites [ks .data ['spike_clusters' ] == unit ],
423477 'spike_depths' : spike_depths [ks .data ['spike_clusters' ] == unit ]})
424478
425- self .insert1 ({** key , 'clustering_time' : creation_time ,
426- 'quality_control' : is_qc , 'manual_curation' : is_curated })
479+ self .insert1 (key )
427480 self .Unit .insert ([{** key , ** u } for u in units ])
428481
429482
430483@schema
431484class Waveform (dj .Imported ):
432485 definition = """
433- -> Clustering .Unit
486+ -> CuratedClustering .Unit
434487 ---
435488 peak_chn_waveform_mean: longblob # mean over all spikes at the peak channel for this unit
436489 """
@@ -446,11 +499,11 @@ class Electrode(dj.Part):
446499
447500 @property
448501 def key_source (self ):
449- return Clustering ()
502+ return Curation ()
450503
451504 def make (self , key ):
452505 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 ' )
454507 ks = kilosort .Kilosort (ks_dir )
455508
456509 acq_software , probe_sn = (EphysRecording * ProbeInsertion & key ).fetch1 ('acq_software' , 'probe' )
@@ -459,10 +512,10 @@ def make(self, key):
459512 rec_key = (EphysRecording & key ).fetch1 ('KEY' )
460513 chn2electrodes = get_neuropixels_chn2electrode_map (rec_key , acq_software )
461514
462- is_qc = (Clustering & key ).fetch1 ('quality_control' )
515+ is_qc = (Curation & key ).fetch1 ('quality_control' )
463516
464517 # 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' )}
466519
467520 unit_waveforms , unit_peak_waveforms = [], []
468521 if is_qc :
@@ -503,7 +556,7 @@ def make(self, key):
503556@schema
504557class ClusterQualityMetrics (dj .Imported ):
505558 definition = """
506- -> Clustering .Unit
559+ -> CuratedClustering .Unit
507560 ---
508561 amp: float
509562 snr: float
0 commit comments