Skip to content

Commit 0e7004d

Browse files
authored
Various improvements (#8)
1 parent d38af0e commit 0e7004d

File tree

7 files changed

+204
-70
lines changed

7 files changed

+204
-70
lines changed

Dockerfile

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@ RUN wget "https://www.wavpack.com/wavpack-${WAVPACK_VERSION}.tar.bz2" && \
2222
# Install
2323
RUN pip install wavpack-numcodecs
2424

25+
# Install spikeinterface from source
26+
RUN git clone https://github.com/SpikeInterface/spikeinterface.git && \
27+
cd spikeinterface && \
28+
git checkout d6f8c5af9d33aca3d9191472205b91adc3ca1faf && \
29+
pip install . && cd ..
30+
2531
# Install spikeinterface-gui from source
26-
RUN git clone https://github.com/alejoe91/spikeinterface-gui.git && \
32+
RUN git clone https://github.com/SpikeInterface/spikeinterface-gui.git && \
2733
cd spikeinterface-gui && \
28-
git checkout 1e1064be602867c6647eb9665c479cde6aca159f && \
34+
git checkout 176c1b12f731d34e320c626d7ec3b1def011c791 && \
2935
pip install . && cd ..
3036

3137

3238
EXPOSE 8000
33-
ENTRYPOINT ["sh", "-c", "panel serve src/aind_ephys_portal/ephys_portal_app.py src/aind_ephys_portal/ephys_gui_app.py --static-dirs images=src/aind_ephys_portal/images --address 0.0.0.0 --port 8000 --allow-websocket-origin ${ALLOW_WEBSOCKET_ORIGIN} --keep-alive 10000 --index ephys_portal_app.py --warm"]
39+
ENTRYPOINT ["sh", "-c", "panel serve src/aind_ephys_portal/ephys_portal_app.py src/aind_ephys_portal/ephys_gui_app.py --setup src/aind_ephys_portal/setup.py --static-dirs images=src/aind_ephys_portal/images --address 0.0.0.0 --port 8000 --allow-websocket-origin ${ALLOW_WEBSOCKET_ORIGIN} --keep-alive 10000 --index ephys_portal_app.py --num-procs 4 --warm"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ To run the Ephys Portal:
2828

2929
```bash
3030
# Run using Panel CLI (recommended for server deployment)
31-
panel serve src/aind_ephys_portal/ephys_portal_app.py src/aind_ephys_portal/ephys_gui_app.py --static-dirs images=src/aind_ephys_portal/images --autoreload
31+
panel serve src/aind_ephys_portal/ephys_portal_app.py src/aind_ephys_portal/ephys_gui_app.py --setup src/aind_ephys_portal/setup.py --static-dirs images=src/aind_ephys_portal/images --num-procs 8
3232
```
3333

3434
This will start a Panel server and make the application available in your web browser.

src/aind_ephys_portal/ephys_gui_app.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import param
55

66
from aind_ephys_portal.panel.ephys_gui import EphysGuiView
7+
from aind_ephys_portal.monitor import monitor
78

89
import panel as pn
910

@@ -29,6 +30,7 @@ class Settings(param.Parameterized):
2930

3031

3132
ephys_gui = EphysGuiView(analyzer_path=settings.analyzer_path, recording_path=settings.recording_path)
32-
ephys_gui_panel = ephys_gui.panel()
33+
ephys_gui.layout.sizing_mode = "stretch_both"
34+
app_layout = pn.Column(monitor, ephys_gui.layout, sizing_mode="stretch_both", min_height=600)
3335

34-
ephys_gui_panel.servable(title="AIND Ephys GUI")
36+
app_layout.servable(title="AIND Ephys GUI")

src/aind_ephys_portal/monitor.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import panel as pn
2+
import psutil
3+
4+
pn.extension()
5+
6+
7+
# --- Memory info ---
8+
def get_mem_info():
9+
mem = psutil.virtual_memory()
10+
total_gb = mem.total / 1e9
11+
used_gb = mem.used / 1e9
12+
return used_gb, total_gb, mem.percent
13+
14+
15+
# --- Progress indicator ---
16+
used_gb, total_gb, percent = get_mem_info()
17+
18+
ram_monitor = pn.widgets.indicators.Progress(
19+
name="Memory Usage",
20+
value=int(used_gb),
21+
max=int(total_gb),
22+
bar_color="success" if percent < 70 else "warning" if percent < 90 else "danger",
23+
width=400,
24+
height=30,
25+
)
26+
27+
cpu_monitor = pn.widgets.indicators.Progress(
28+
name="CPU Usage",
29+
value=int(psutil.cpu_percent()),
30+
max=100,
31+
bar_color="success" if psutil.cpu_percent() < 70 else "warning" if psutil.cpu_percent() < 90 else "danger",
32+
width=400,
33+
height=30,
34+
)
35+
36+
37+
# --- Periodic update ---
38+
def update_usage():
39+
used_gb, total_gb, percent = get_mem_info()
40+
ram_monitor.value = int(used_gb)
41+
ram_monitor.bar_color = "success" if percent < 70 else "warning" if percent < 90 else "danger"
42+
cpu_percent = psutil.cpu_percent()
43+
cpu_monitor.value = int(cpu_percent)
44+
cpu_monitor.bar_color = "success" if cpu_percent < 70 else "warning" if cpu_percent < 90 else "danger"
45+
46+
pn.state.add_periodic_callback(update_usage, period=2000)
47+
48+
ram_usage_label = pn.widgets.StaticText(value="🐏 RAM Usage", height=30)
49+
cpu_usage_label = pn.widgets.StaticText(value="🖥️ CPU Usage", height=30)
50+
active_user_count_label = pn.widgets.StaticText(value="🧑‍🔬 Active Users", height=30)
51+
active_user_count = pn.widgets.StaticText(value="0", height=30)
52+
53+
54+
def update_user_count():
55+
active_user_count.value = str(len(pn.state.active_sessions))
56+
57+
58+
pn.state.add_periodic_callback(update_user_count, period=2000)
59+
60+
monitor = pn.Row(
61+
ram_usage_label,
62+
ram_monitor,
63+
cpu_usage_label,
64+
cpu_monitor,
65+
active_user_count_label,
66+
active_user_count,
67+
sizing_mode="stretch_width"
68+
)

src/aind_ephys_portal/panel/ephys_gui.py

Lines changed: 81 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import param
33
import boto3
44
import time
5-
from copy import deepcopy
65

76
import panel as pn
87

@@ -15,6 +14,8 @@
1514

1615
import spikeinterface as si
1716
from 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

1920
from .utils import Tee
2021

@@ -34,17 +35,24 @@
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
3947
aind_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:\nAnalyzer path: {self.analyzer_path}\nRecording 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

Comments
 (0)