22import param
33import boto3
44import time
5- from copy import deepcopy
65
76import panel as pn
87
1514
1615import spikeinterface as si
1716from spikeinterface .core .core_tools import extractor_dict_iterator , set_value_in_extractor_dict
17+ from spikeinterface .curation import validate_curation_dict
18+ from spikeinterface_gui .curation_tools import empty_curation_data , default_label_definitions
1819
1920from .utils import Tee
2021
3435 "splits" : [],
3536}
3637
38+ help_txt = """
39+ ## Usage
40+ Sorting Analyzer not loaded. Follow the steps below to launch the SpikeInterface GUI:
41+ 1. Enter the path to the SpikeInterface analyzer Zarr file.
42+ 2. (Optional) Enter the path to the processed recording folder.
43+ 3. Click "Launch!" to start the SpikeInterface GUI.
44+ """
3745
3846# Define the layout for the AIND Ephys GUI
3947aind_layout = dict (
40- zone1 = [' unitlist' , ' curation' , 'mergelist' , ' spikelist' ],
48+ zone1 = [" unitlist" , " curation" , "merge" , " spikelist" ],
4149 zone2 = [],
42- zone3 = [' spikeamplitude' , ' spikedepth' , ' trace' , ' tracemap' ],
50+ zone3 = [" spikeamplitude" , " spikedepth" , "spikerate" , " trace" , " tracemap" ],
4351 zone4 = [],
44- zone5 = [' probe' ],
45- zone6 = [' ndscatter' , ' similarity' ],
46- zone7 = [' waveform' ],
47- zone8 = [' correlogram' ],
52+ zone5 = [" probe" ],
53+ zone6 = [" ndscatter" , " similarity" ],
54+ zone7 = [" waveform" , "waveformheatmap" ],
55+ zone8 = [" correlogram" , "metrics" , "mainsettings" ],
4856)
4957
5058
@@ -59,60 +67,73 @@ def __init__(self, analyzer_path, recording_path, **params):
5967 self .recording_path = recording_path
6068 self .analyzer = None
6169
70+ self .analyzer_input = pn .widgets .TextInput (
71+ name = "Analyzer path" , value = self .analyzer_path , height = 50 , sizing_mode = "stretch_width"
72+ )
73+ self .recording_input = pn .widgets .TextInput (
74+ name = "Recording path (optional)" , value = self .recording_path , height = 50 , sizing_mode = "stretch_width"
75+ )
76+ self .launch_button = pn .widgets .Button (
77+ name = "Launch!" , button_type = "primary" , height = 50 , sizing_mode = "stretch_width"
78+ )
79+
80+ self .spinner = pn .indicators .LoadingSpinner (value = True , sizing_mode = "stretch_width" )
81+ self .log_output_text = pn .widgets .TextAreaInput (value = "" , sizing_mode = "stretch_both" )
82+ clear_log_button = pn .widgets .Button (name = "Clear Log" , button_type = "warning" , sizing_mode = "stretch_width" )
83+ clear_log_button .on_click (self ._clear_log )
84+ self .log_output = pn .Column (self .log_output_text , clear_log_button , sizing_mode = "stretch_both" )
85+
86+ original_stdout = sys .stdout
87+ sys .stdout = Tee (original_stdout , self .log_output_text )
88+ original_stderr = sys .stderr
89+ sys .stderr = Tee (original_stderr , self .log_output_text )
90+
91+ self .loading_banner = pn .Row (self .spinner , self .log_output , sizing_mode = "stretch_both" )
92+
93+ self .top_panel = pn .Row (
94+ self .analyzer_input ,
95+ self .recording_input ,
96+ self .launch_button ,
97+ sizing_mode = "stretch_width" ,
98+ )
99+
62100 # Create initial layout
63101 self .layout = pn .Column (
64- pn .Row (
65- pn .widgets .TextInput (
66- name = "Analyzer path" , value = self .analyzer_path , height = 50 , sizing_mode = "stretch_width"
67- ),
68- pn .widgets .TextInput (
69- name = "Recording path (optional)" , value = self .recording_path , height = 50 , sizing_mode = "stretch_width"
70- ),
71- pn .widgets .Button (name = "Launch!" , button_type = "primary" , height = 50 , sizing_mode = "stretch_width" ),
72- sizing_mode = "stretch_width" ,
73- ),
102+ self .top_panel ,
74103 self ._create_main_window (),
104+ sizing_mode = "stretch_both" ,
75105 )
76106
77- # Store widget references
78- self .analyzer_input = self .layout [0 ][0 ]
79- self .recording_input = self .layout [0 ][1 ]
80- self .launch_button = self .layout [0 ][2 ]
81-
82107 # Setup event handlers
83108 self .analyzer_input .param .watch (self .update_values , "value" )
84109 self .recording_input .param .watch (self .update_values , "value" )
85110 self .launch_button .on_click (self .on_click )
86111
112+ if self .analyzer_path != "" :
113+ # # # Schedule initialization to run after UI is rendered
114+ def delayed_init ():
115+ self ._initialize ()
116+ return False # Don't repeat the callback
117+ pn .state .add_periodic_callback (delayed_init , period = 500 , count = 1 )
118+
87119 def _initialize (self ):
120+ self .layout [1 ] = self .loading_banner
121+ self .log_output_text .value = ""
88122 if self .analyzer_input .value != "" :
89123 t_start = time .perf_counter ()
90- spinner = pn .indicators .LoadingSpinner (value = True , sizing_mode = "stretch_width" )
91- # Create a TextArea widget to display logs
92- log_output = pn .widgets .TextAreaInput (value = "" , sizing_mode = "stretch_both" )
93-
94- original_stdout = sys .stdout
95- sys .stdout = Tee (original_stdout , log_output ) # Redirect stdout
96-
97- original_stderr = sys .stderr
98- sys .stderr = Tee (original_stderr , log_output ) # Redirect stderr
99-
100- self .layout [1 ] = pn .Row (spinner , log_output )
101-
102124 print (
103125 f"Initializing Ephys GUI for:\n Analyzer path: { self .analyzer_path } \n Recording path: { self .recording_path } "
104126 )
105-
106- self .analyzer = si .load (self .analyzer_path , load_extensions = False )
107- if self .recording_path and self .recording_path != "" and not self .analyzer .has_recording ():
127+ self ._initialize_analyzer ()
128+ if self .recording_path != "" :
108129 self ._set_processed_recording ()
109130 self .win = self ._create_main_window ()
110131 self .layout [1 ] = self .win
111132 print ("Ephys GUI initialized successfully!" )
112133 t_stop = time .perf_counter ()
113134 print (f"Initialization time: { t_stop - t_start :.2f} seconds" )
114- sys . stdout = sys . __stdout__ # Reset stdout
115- sys . stderr = sys . __stderr__ # Reset stderr
135+ else :
136+ print ( "Analyzer path is empty. Please provide a valid path." )
116137
117138 def _initialize_analyzer (self ):
118139 if not self .analyzer_path .endswith ((".zarr" , ".zarr/" )):
@@ -142,7 +163,7 @@ def _set_processed_recording(self):
142163 def _create_main_window (self ):
143164 if self .analyzer is not None :
144165 # prepare the curation data using decoder labels
145- curation_dict = deepcopy ( default_curation_dict )
166+ curation_dict = default_curation_dict
146167 curation_dict ["unit_ids" ] = self .analyzer .unit_ids
147168 if "decoder_label" in self .analyzer .sorting .get_property_keys ():
148169 decoder_labels = self .analyzer .get_sorting_property ("decoder_label" )
@@ -151,6 +172,12 @@ def _create_main_window(self):
151172 for unit_id in noise_units :
152173 curation_dict ["manual_labels" ].append ({"unit_id" : unit_id , "quality" : ["noise" ]})
153174
175+ try :
176+ validate_curation_dict (curation_dict )
177+ except ValueError as e :
178+ print (f"Curated dictionary is invalid: { e } " )
179+ curation_dict = None
180+
154181 win = run_mainwindow (
155182 analyzer = self .analyzer ,
156183 curation = True ,
@@ -159,19 +186,29 @@ def _create_main_window(self):
159186 curation_dict = curation_dict ,
160187 mode = "web" ,
161188 start_app = False ,
189+ panel_window_servable = False ,
162190 verbose = True ,
163- layout = aind_layout ,
164- panel_window_servable = False
191+ layout = aind_layout
165192 )
166- return win .main_layout
193+ self .log_output .value = ""
194+ tabs = pn .Tabs (
195+ ("GUI" , win .main_layout ),
196+ ("Log" , self .log_output ),
197+ tabs_location = "below" ,
198+ sizing_mode = "stretch_both" ,
199+ )
200+ return tabs
167201 else :
168- return pn .pane .Markdown ("Analyzer not initialized " )
202+ return pn .pane .Markdown (help_txt , sizing_mode = "stretch_both " )
169203
170204 def update_values (self , event ):
171205 self .analyzer_path = self .analyzer_input .value
172206 self .recording_path = self .recording_input .value
173207 self ._initialize ()
174208
209+ def _clear_log (self , event ):
210+ self .log_output_text .value = ""
211+
175212 def on_click (self , event ):
176213 print ("Launching SpikeInterface GUI!" )
177214 self ._initialize ()
0 commit comments