canns 0.12.7__py3-none-any.whl → 0.13.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- canns/analyzer/data/__init__.py +3 -11
- canns/analyzer/data/asa/__init__.py +84 -0
- canns/analyzer/data/asa/cohospace.py +905 -0
- canns/analyzer/data/asa/config.py +246 -0
- canns/analyzer/data/asa/decode.py +445 -0
- canns/analyzer/data/asa/embedding.py +269 -0
- canns/analyzer/data/asa/filters.py +208 -0
- canns/analyzer/data/{cann1d.py → asa/fly_roi.py} +98 -45
- canns/analyzer/data/asa/fr.py +431 -0
- canns/analyzer/data/asa/path.py +389 -0
- canns/analyzer/data/asa/plotting.py +1287 -0
- canns/analyzer/data/asa/tda.py +901 -0
- canns/analyzer/visualization/core/backend.py +1 -1
- canns/analyzer/visualization/core/config.py +77 -0
- canns/analyzer/visualization/core/rendering.py +10 -6
- canns/analyzer/visualization/energy_plots.py +22 -8
- canns/analyzer/visualization/spatial_plots.py +31 -11
- canns/analyzer/visualization/theta_sweep_plots.py +15 -6
- canns/pipeline/__init__.py +4 -8
- canns/pipeline/asa/__init__.py +21 -0
- canns/pipeline/asa/__main__.py +11 -0
- canns/pipeline/asa/app.py +1000 -0
- canns/pipeline/asa/runner.py +1095 -0
- canns/pipeline/asa/screens.py +215 -0
- canns/pipeline/asa/state.py +248 -0
- canns/pipeline/asa/styles.tcss +221 -0
- canns/pipeline/asa/widgets.py +233 -0
- canns/pipeline/gallery/__init__.py +7 -0
- canns/task/open_loop_navigation.py +3 -1
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/METADATA +6 -3
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/RECORD +34 -17
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/entry_points.txt +1 -0
- canns/analyzer/data/cann2d.py +0 -2565
- canns/pipeline/theta_sweep.py +0 -573
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/WHEEL +0 -0
- {canns-0.12.7.dist-info → canns-0.13.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
"""Main ASA TUI application with two-page workflow.
|
|
2
|
+
|
|
3
|
+
This module provides the main Textual application for ASA analysis,
|
|
4
|
+
following the original GUI's two-page structure:
|
|
5
|
+
1. PreprocessPage - File selection and preprocessing
|
|
6
|
+
2. AnalysisPage - Analysis mode selection and execution
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from textual.app import App, ComposeResult
|
|
12
|
+
from textual.binding import Binding
|
|
13
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
14
|
+
from textual.widgets import (
|
|
15
|
+
Button,
|
|
16
|
+
Checkbox,
|
|
17
|
+
DirectoryTree,
|
|
18
|
+
Footer,
|
|
19
|
+
Header,
|
|
20
|
+
Input,
|
|
21
|
+
Label,
|
|
22
|
+
ProgressBar,
|
|
23
|
+
Select,
|
|
24
|
+
Static,
|
|
25
|
+
TabbedContent,
|
|
26
|
+
TabPane,
|
|
27
|
+
)
|
|
28
|
+
from textual.worker import Worker
|
|
29
|
+
|
|
30
|
+
from .runner import PipelineRunner
|
|
31
|
+
from .screens import ErrorScreen, HelpScreen, TerminalSizeWarning, WorkdirScreen
|
|
32
|
+
from .state import WorkflowState, get_preset_params, relative_path, validate_files
|
|
33
|
+
from .widgets import ImagePreview, LogViewer, ParamGroup
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ASAApp(App):
|
|
37
|
+
"""Main TUI application for ASA analysis."""
|
|
38
|
+
|
|
39
|
+
CSS_PATH = "styles.tcss"
|
|
40
|
+
TITLE = "Attractor Structure Analyzer (ASA)"
|
|
41
|
+
|
|
42
|
+
# Terminal size requirements
|
|
43
|
+
MIN_WIDTH = 100
|
|
44
|
+
RECOMMENDED_WIDTH = 120
|
|
45
|
+
MIN_HEIGHT = 30
|
|
46
|
+
RECOMMENDED_HEIGHT = 40
|
|
47
|
+
|
|
48
|
+
BINDINGS = [
|
|
49
|
+
Binding("ctrl+w", "change_workdir", "Workdir"),
|
|
50
|
+
Binding("ctrl+r", "run_action", "Run"),
|
|
51
|
+
Binding("f5", "refresh", "Refresh"),
|
|
52
|
+
Binding("question_mark", "help", "Help"),
|
|
53
|
+
Binding("escape", "quit", "Quit"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def __init__(self):
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.state = WorkflowState()
|
|
59
|
+
self.runner = PipelineRunner()
|
|
60
|
+
self.current_worker: Worker = None
|
|
61
|
+
self._size_warning_shown = False
|
|
62
|
+
self.current_page = "preprocess" # "preprocess" or "analysis"
|
|
63
|
+
|
|
64
|
+
def compose(self) -> ComposeResult:
|
|
65
|
+
"""Compose the main UI layout."""
|
|
66
|
+
yield Header()
|
|
67
|
+
|
|
68
|
+
with Horizontal(id="main-container"):
|
|
69
|
+
# Left panel (controls)
|
|
70
|
+
with Vertical(id="left-panel"):
|
|
71
|
+
yield Label(f"Workdir: {self.state.workdir}", id="workdir-label")
|
|
72
|
+
yield Button("Change Workdir", id="change-workdir-btn")
|
|
73
|
+
|
|
74
|
+
# Page indicator
|
|
75
|
+
yield Label("Page: Preprocess", id="page-indicator")
|
|
76
|
+
|
|
77
|
+
# Action buttons and progress (OUTSIDE scroll area)
|
|
78
|
+
with Vertical(id="actions-bar"):
|
|
79
|
+
yield Button("Continue →", variant="primary", id="continue-btn")
|
|
80
|
+
yield Button("← Back", variant="primary", id="back-btn", classes="hidden")
|
|
81
|
+
yield Button(
|
|
82
|
+
"Run Analysis", variant="primary", id="run-analysis-btn", classes="hidden"
|
|
83
|
+
)
|
|
84
|
+
yield Button(
|
|
85
|
+
"Stop", variant="error", id="stop-btn", classes="hidden", disabled=True
|
|
86
|
+
)
|
|
87
|
+
yield ProgressBar(id="progress-bar")
|
|
88
|
+
yield Static("Status: Idle", id="run-status")
|
|
89
|
+
|
|
90
|
+
# Middle panel (parameters + file browser)
|
|
91
|
+
with Horizontal(id="middle-panel"):
|
|
92
|
+
with Vertical(id="params-panel"):
|
|
93
|
+
yield Label("Parameters", id="params-header")
|
|
94
|
+
with VerticalScroll(id="controls-scroll"):
|
|
95
|
+
# Preprocess controls - single param group
|
|
96
|
+
with Vertical(id="preprocess-controls"):
|
|
97
|
+
with ParamGroup("Input & Preprocess"):
|
|
98
|
+
# Input section
|
|
99
|
+
yield Label("Input Mode:")
|
|
100
|
+
yield Select(
|
|
101
|
+
[("ASA File", "asa"), ("Neuron + Traj", "neuron_traj")],
|
|
102
|
+
value="asa",
|
|
103
|
+
id="input-mode-select",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
yield Label("Preset:")
|
|
107
|
+
yield Select(
|
|
108
|
+
[("Grid", "grid"), ("HD", "hd"), ("None", "none")],
|
|
109
|
+
value="grid",
|
|
110
|
+
id="preset-select",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Preprocess section
|
|
114
|
+
yield Label("Method:")
|
|
115
|
+
yield Select(
|
|
116
|
+
[
|
|
117
|
+
("None", "none"),
|
|
118
|
+
("Embed Spike Trains", "embed_spike_trains"),
|
|
119
|
+
],
|
|
120
|
+
value="none",
|
|
121
|
+
id="preprocess-method-select",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Preprocessing parameters (enabled when method is embed_spike_trains)
|
|
125
|
+
with Vertical(id="emb-params"):
|
|
126
|
+
yield Label("res:", id="emb-res-label")
|
|
127
|
+
yield Input(value="100000", id="emb-res", disabled=True)
|
|
128
|
+
|
|
129
|
+
yield Label("dt:", id="emb-dt-label")
|
|
130
|
+
yield Input(value="1000", id="emb-dt", disabled=True)
|
|
131
|
+
|
|
132
|
+
yield Label("sigma:", id="emb-sigma-label")
|
|
133
|
+
yield Input(value="5000", id="emb-sigma", disabled=True)
|
|
134
|
+
|
|
135
|
+
yield Checkbox(
|
|
136
|
+
"smooth", id="emb-smooth", value=True, disabled=True
|
|
137
|
+
)
|
|
138
|
+
yield Checkbox(
|
|
139
|
+
"speed_filter",
|
|
140
|
+
id="emb-speed-filter",
|
|
141
|
+
value=False,
|
|
142
|
+
disabled=True,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
yield Label("min_speed:", id="emb-min-speed-label")
|
|
146
|
+
yield Input(value="2.5", id="emb-min-speed", disabled=True)
|
|
147
|
+
|
|
148
|
+
# Analysis controls (initially hidden)
|
|
149
|
+
with Vertical(id="analysis-controls", classes="hidden"):
|
|
150
|
+
preset_params = get_preset_params(self.state.preset)
|
|
151
|
+
tda_defaults = preset_params.get("tda", {})
|
|
152
|
+
grid_defaults = preset_params.get("gridscore", {})
|
|
153
|
+
|
|
154
|
+
with ParamGroup("Analysis Mode"):
|
|
155
|
+
yield Label("Mode:")
|
|
156
|
+
yield Select(
|
|
157
|
+
[
|
|
158
|
+
("TDA", "tda"),
|
|
159
|
+
("CohoMap", "cohomap"),
|
|
160
|
+
("PathCompare", "pathcompare"),
|
|
161
|
+
("CohoSpace", "cohospace"),
|
|
162
|
+
("FR", "fr"),
|
|
163
|
+
("FRM", "frm"),
|
|
164
|
+
("GridScore", "gridscore"),
|
|
165
|
+
],
|
|
166
|
+
value=self.state.analysis_mode,
|
|
167
|
+
id="analysis-mode-select",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
with ParamGroup("TDA Parameters", id="analysis-params-tda"):
|
|
171
|
+
yield Label("dim:")
|
|
172
|
+
yield Input(value=str(tda_defaults.get("dim", 6)), id="tda-dim")
|
|
173
|
+
yield Label("num_times:")
|
|
174
|
+
yield Input(
|
|
175
|
+
value=str(tda_defaults.get("num_times", 5)), id="tda-num-times"
|
|
176
|
+
)
|
|
177
|
+
yield Label("active_times:")
|
|
178
|
+
yield Input(
|
|
179
|
+
value=str(tda_defaults.get("active_times", 15000)),
|
|
180
|
+
id="tda-active-times",
|
|
181
|
+
)
|
|
182
|
+
yield Label("k:")
|
|
183
|
+
yield Input(value=str(tda_defaults.get("k", 1000)), id="tda-k")
|
|
184
|
+
yield Label("n_points:")
|
|
185
|
+
yield Input(
|
|
186
|
+
value=str(tda_defaults.get("n_points", 1200)), id="tda-n-points"
|
|
187
|
+
)
|
|
188
|
+
yield Label("metric:")
|
|
189
|
+
yield Select(
|
|
190
|
+
[
|
|
191
|
+
("cosine", "cosine"),
|
|
192
|
+
("euclidean", "euclidean"),
|
|
193
|
+
("correlation", "correlation"),
|
|
194
|
+
],
|
|
195
|
+
value=str(tda_defaults.get("metric", "cosine")),
|
|
196
|
+
id="tda-metric",
|
|
197
|
+
)
|
|
198
|
+
yield Label("nbs:")
|
|
199
|
+
yield Input(value=str(tda_defaults.get("nbs", 800)), id="tda-nbs")
|
|
200
|
+
yield Label("maxdim:")
|
|
201
|
+
yield Input(
|
|
202
|
+
value=str(tda_defaults.get("maxdim", 1)), id="tda-maxdim"
|
|
203
|
+
)
|
|
204
|
+
yield Label("coeff:")
|
|
205
|
+
yield Input(
|
|
206
|
+
value=str(tda_defaults.get("coeff", 47)), id="tda-coeff"
|
|
207
|
+
)
|
|
208
|
+
yield Checkbox(
|
|
209
|
+
"do_shuffle",
|
|
210
|
+
id="tda-do-shuffle",
|
|
211
|
+
value=tda_defaults.get("do_shuffle", False),
|
|
212
|
+
)
|
|
213
|
+
yield Label("num_shuffles:")
|
|
214
|
+
yield Input(
|
|
215
|
+
value=str(tda_defaults.get("num_shuffles", 1000)),
|
|
216
|
+
id="tda-num-shuffles",
|
|
217
|
+
)
|
|
218
|
+
yield Checkbox(
|
|
219
|
+
"standardize (StandardScaler)", id="tda-standardize", value=True
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
with ParamGroup(
|
|
223
|
+
"Decode / CohoMap", id="analysis-params-decode", classes="hidden"
|
|
224
|
+
):
|
|
225
|
+
yield Label("decode_version:")
|
|
226
|
+
yield Select(
|
|
227
|
+
[
|
|
228
|
+
("v2 (multi)", "v2"),
|
|
229
|
+
("v0 (legacy)", "v0"),
|
|
230
|
+
],
|
|
231
|
+
value="v2",
|
|
232
|
+
id="decode-version",
|
|
233
|
+
)
|
|
234
|
+
yield Label("num_circ:")
|
|
235
|
+
yield Input(value="2", id="decode-num-circ")
|
|
236
|
+
yield Label("cohomap_subsample:")
|
|
237
|
+
yield Input(value="10", id="cohomap-subsample")
|
|
238
|
+
yield Checkbox(
|
|
239
|
+
"real_ground (v0)", id="decode-real-ground", value=True
|
|
240
|
+
)
|
|
241
|
+
yield Checkbox("real_of (v0)", id="decode-real-of", value=True)
|
|
242
|
+
|
|
243
|
+
with ParamGroup(
|
|
244
|
+
"PathCompare Parameters",
|
|
245
|
+
id="analysis-params-pathcompare",
|
|
246
|
+
classes="hidden",
|
|
247
|
+
):
|
|
248
|
+
yield Checkbox(
|
|
249
|
+
"use_box (coordsbox/times_box)", id="pc-use-box", value=True
|
|
250
|
+
)
|
|
251
|
+
yield Checkbox(
|
|
252
|
+
"interp_to_full (use_box)", id="pc-interp-full", value=True
|
|
253
|
+
)
|
|
254
|
+
yield Label("dim_mode:")
|
|
255
|
+
yield Select(
|
|
256
|
+
[("2d", "2d"), ("1d", "1d")],
|
|
257
|
+
value="2d",
|
|
258
|
+
id="pc-dim-mode",
|
|
259
|
+
)
|
|
260
|
+
yield Label("dim (1d):")
|
|
261
|
+
yield Input(value="1", id="pc-dim")
|
|
262
|
+
yield Label("dim1 (2d):")
|
|
263
|
+
yield Input(value="1", id="pc-dim1")
|
|
264
|
+
yield Label("dim2 (2d):")
|
|
265
|
+
yield Input(value="2", id="pc-dim2")
|
|
266
|
+
yield Label("coords_key (optional):")
|
|
267
|
+
yield Input(value="", id="pc-coords-key")
|
|
268
|
+
yield Label("times_box_key (optional):")
|
|
269
|
+
yield Input(value="", id="pc-times-key")
|
|
270
|
+
yield Label("slice_mode:")
|
|
271
|
+
yield Select(
|
|
272
|
+
[("time", "time"), ("index", "index")],
|
|
273
|
+
value="time",
|
|
274
|
+
id="pc-slice-mode",
|
|
275
|
+
)
|
|
276
|
+
yield Label("tmin (sec, -1=auto):")
|
|
277
|
+
yield Input(value="-1", id="pc-tmin")
|
|
278
|
+
yield Label("tmax (sec, -1=auto):")
|
|
279
|
+
yield Input(value="-1", id="pc-tmax")
|
|
280
|
+
yield Label("imin (-1=auto):")
|
|
281
|
+
yield Input(value="-1", id="pc-imin")
|
|
282
|
+
yield Label("imax (-1=auto):")
|
|
283
|
+
yield Input(value="-1", id="pc-imax")
|
|
284
|
+
yield Label("stride:")
|
|
285
|
+
yield Input(value="1", id="pc-stride")
|
|
286
|
+
yield Label("theta_scale (rad/deg/unit/auto):")
|
|
287
|
+
yield Input(value="rad", id="pathcompare-angle-scale")
|
|
288
|
+
|
|
289
|
+
with ParamGroup(
|
|
290
|
+
"CohoSpace Parameters",
|
|
291
|
+
id="analysis-params-cohospace",
|
|
292
|
+
classes="hidden",
|
|
293
|
+
):
|
|
294
|
+
yield Label("dim_mode:")
|
|
295
|
+
yield Select(
|
|
296
|
+
[("2d", "2d"), ("1d", "1d")],
|
|
297
|
+
value="2d",
|
|
298
|
+
id="coho-dim-mode",
|
|
299
|
+
)
|
|
300
|
+
yield Label("dim (1d):")
|
|
301
|
+
yield Input(value="1", id="coho-dim")
|
|
302
|
+
yield Label("dim1 (2d):")
|
|
303
|
+
yield Input(value="1", id="coho-dim1")
|
|
304
|
+
yield Label("dim2 (2d):")
|
|
305
|
+
yield Input(value="2", id="coho-dim2")
|
|
306
|
+
yield Label("mode (fr/spike):")
|
|
307
|
+
yield Select(
|
|
308
|
+
[("fr", "fr"), ("spike", "spike")],
|
|
309
|
+
value="fr",
|
|
310
|
+
id="coho-mode",
|
|
311
|
+
)
|
|
312
|
+
yield Label("top_percent (fr):")
|
|
313
|
+
yield Input(value="5.0", id="coho-top-percent")
|
|
314
|
+
yield Label("view:")
|
|
315
|
+
yield Select(
|
|
316
|
+
[
|
|
317
|
+
("both", "both"),
|
|
318
|
+
("single", "single"),
|
|
319
|
+
("population", "population"),
|
|
320
|
+
],
|
|
321
|
+
value="both",
|
|
322
|
+
id="coho-view",
|
|
323
|
+
)
|
|
324
|
+
yield Label("neuron_id (optional):")
|
|
325
|
+
yield Input(value="", id="cohospace-neuron-id")
|
|
326
|
+
yield Label("subsample (trajectory):")
|
|
327
|
+
yield Input(value="2", id="coho-subsample")
|
|
328
|
+
yield Label("unfold:")
|
|
329
|
+
yield Select(
|
|
330
|
+
[("square", "square"), ("skew", "skew")],
|
|
331
|
+
value="square",
|
|
332
|
+
id="coho-unfold",
|
|
333
|
+
)
|
|
334
|
+
yield Checkbox(
|
|
335
|
+
"skew_show_grid", id="coho-skew-show-grid", value=True
|
|
336
|
+
)
|
|
337
|
+
yield Label("skew_tiles:")
|
|
338
|
+
yield Input(value="0", id="coho-skew-tiles")
|
|
339
|
+
|
|
340
|
+
with ParamGroup(
|
|
341
|
+
"FR Parameters", id="analysis-params-fr", classes="hidden"
|
|
342
|
+
):
|
|
343
|
+
yield Label("neuron_start:")
|
|
344
|
+
yield Input(value="", id="fr-neuron-start")
|
|
345
|
+
yield Label("neuron_end:")
|
|
346
|
+
yield Input(value="", id="fr-neuron-end")
|
|
347
|
+
yield Label("time_start:")
|
|
348
|
+
yield Input(value="", id="fr-time-start")
|
|
349
|
+
yield Label("time_end:")
|
|
350
|
+
yield Input(value="", id="fr-time-end")
|
|
351
|
+
yield Label("mode (fr/spike):")
|
|
352
|
+
yield Select(
|
|
353
|
+
[("fr", "fr"), ("spike", "spike")],
|
|
354
|
+
value="fr",
|
|
355
|
+
id="fr-mode",
|
|
356
|
+
)
|
|
357
|
+
yield Label("normalize:")
|
|
358
|
+
yield Select(
|
|
359
|
+
[
|
|
360
|
+
("zscore_per_neuron", "zscore_per_neuron"),
|
|
361
|
+
("minmax_per_neuron", "minmax_per_neuron"),
|
|
362
|
+
("none", "none"),
|
|
363
|
+
],
|
|
364
|
+
value="none",
|
|
365
|
+
id="fr-normalize",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
with ParamGroup(
|
|
369
|
+
"FRM Parameters", id="analysis-params-frm", classes="hidden"
|
|
370
|
+
):
|
|
371
|
+
yield Label("neuron_id:")
|
|
372
|
+
yield Input(value="0", id="frm-neuron-id")
|
|
373
|
+
yield Label("bins:")
|
|
374
|
+
yield Input(value="50", id="frm-bins")
|
|
375
|
+
yield Label("min_occupancy:")
|
|
376
|
+
yield Input(value="1", id="frm-min-occupancy")
|
|
377
|
+
yield Checkbox("smoothing", id="frm-smoothing", value=False)
|
|
378
|
+
yield Label("smooth_sigma:")
|
|
379
|
+
yield Input(value="2.0", id="frm-smooth-sigma")
|
|
380
|
+
yield Label("mode (fr/spike):")
|
|
381
|
+
yield Select(
|
|
382
|
+
[("fr", "fr"), ("spike", "spike")],
|
|
383
|
+
value="fr",
|
|
384
|
+
id="frm-mode",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
with ParamGroup(
|
|
388
|
+
"GridScore Parameters",
|
|
389
|
+
id="analysis-params-gridscore",
|
|
390
|
+
classes="hidden",
|
|
391
|
+
):
|
|
392
|
+
yield Label("annulus_inner:")
|
|
393
|
+
yield Input(
|
|
394
|
+
value=str(grid_defaults.get("annulus_inner", 0.3)),
|
|
395
|
+
id="gridscore-annulus-inner",
|
|
396
|
+
)
|
|
397
|
+
yield Label("annulus_outer:")
|
|
398
|
+
yield Input(
|
|
399
|
+
value=str(grid_defaults.get("annulus_outer", 0.7)),
|
|
400
|
+
id="gridscore-annulus-outer",
|
|
401
|
+
)
|
|
402
|
+
yield Label("bin_size:")
|
|
403
|
+
yield Input(
|
|
404
|
+
value=str(grid_defaults.get("bin_size", 2.5)),
|
|
405
|
+
id="gridscore-bin-size",
|
|
406
|
+
)
|
|
407
|
+
yield Label("smooth_sigma:")
|
|
408
|
+
yield Input(
|
|
409
|
+
value=str(grid_defaults.get("smooth_sigma", 2.0)),
|
|
410
|
+
id="gridscore-smooth-sigma",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
with Vertical(id="file-tree-panel"):
|
|
414
|
+
yield Label("Files in Workdir", id="files-header")
|
|
415
|
+
yield DirectoryTree(self.state.workdir, id="file-tree")
|
|
416
|
+
|
|
417
|
+
# Right panel (results + log at bottom)
|
|
418
|
+
with Vertical(id="right-panel"):
|
|
419
|
+
with TabbedContent(id="results-tabs"):
|
|
420
|
+
with TabPane("Setup", id="setup-tab"):
|
|
421
|
+
yield Static(
|
|
422
|
+
"1. Select working directory (Ctrl-W)\n"
|
|
423
|
+
"2. Choose input mode and files\n"
|
|
424
|
+
"3. Configure preprocessing\n"
|
|
425
|
+
"4. Click 'Continue' to proceed to analysis",
|
|
426
|
+
id="setup-content",
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
with TabPane("Results", id="results-tab"):
|
|
430
|
+
yield ImagePreview(id="result-preview")
|
|
431
|
+
yield Static(
|
|
432
|
+
"No results yet. Complete preprocessing and run analysis.",
|
|
433
|
+
id="result-status",
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Log viewer at bottom (25% height)
|
|
437
|
+
yield LogViewer(id="log-viewer")
|
|
438
|
+
|
|
439
|
+
yield Footer()
|
|
440
|
+
|
|
441
|
+
def on_mount(self) -> None:
|
|
442
|
+
"""Handle app mount event."""
|
|
443
|
+
self.update_workdir_label()
|
|
444
|
+
self.check_terminal_size()
|
|
445
|
+
self.apply_preset_params()
|
|
446
|
+
self.update_analysis_params_visibility()
|
|
447
|
+
self.update_decode_controls()
|
|
448
|
+
self.update_pathcompare_controls()
|
|
449
|
+
self.update_cohospace_controls()
|
|
450
|
+
|
|
451
|
+
def check_terminal_size(self) -> None:
|
|
452
|
+
"""Check terminal size and show warning if too small."""
|
|
453
|
+
size = self.size
|
|
454
|
+
width = size.width
|
|
455
|
+
height = size.height
|
|
456
|
+
|
|
457
|
+
# Adjust layout based on terminal size
|
|
458
|
+
left_panel = self.query_one("#left-panel")
|
|
459
|
+
|
|
460
|
+
if width < self.RECOMMENDED_WIDTH:
|
|
461
|
+
if width < self.MIN_WIDTH:
|
|
462
|
+
# Very small terminal
|
|
463
|
+
left_panel.styles.width = 20
|
|
464
|
+
else:
|
|
465
|
+
# Small terminal
|
|
466
|
+
left_panel.styles.width = 22
|
|
467
|
+
else:
|
|
468
|
+
# Normal/large terminal
|
|
469
|
+
left_panel.styles.width = 22
|
|
470
|
+
|
|
471
|
+
# Show warning if terminal is too small (only once)
|
|
472
|
+
if not self._size_warning_shown and (width < self.MIN_WIDTH or height < self.MIN_HEIGHT):
|
|
473
|
+
self._size_warning_shown = True
|
|
474
|
+
self.push_screen(TerminalSizeWarning(width, height))
|
|
475
|
+
|
|
476
|
+
def on_resize(self, event) -> None:
|
|
477
|
+
"""Handle terminal resize events."""
|
|
478
|
+
self.check_terminal_size()
|
|
479
|
+
|
|
480
|
+
def action_change_workdir(self) -> None:
|
|
481
|
+
"""Change working directory."""
|
|
482
|
+
self.push_screen(WorkdirScreen(), self.on_workdir_selected)
|
|
483
|
+
|
|
484
|
+
def on_workdir_selected(self, path: Path | None) -> None:
|
|
485
|
+
"""Handle workdir selection."""
|
|
486
|
+
if path:
|
|
487
|
+
self.state.workdir = path
|
|
488
|
+
self.runner.reset_input()
|
|
489
|
+
self.update_workdir_label()
|
|
490
|
+
|
|
491
|
+
# Update file tree in middle panel
|
|
492
|
+
tree = self.query_one("#file-tree", DirectoryTree)
|
|
493
|
+
tree.path = path
|
|
494
|
+
tree.reload()
|
|
495
|
+
|
|
496
|
+
def update_workdir_label(self) -> None:
|
|
497
|
+
"""Update the workdir label."""
|
|
498
|
+
label = self.query_one("#workdir-label", Label)
|
|
499
|
+
label.update(f"Workdir: {self.state.workdir}")
|
|
500
|
+
|
|
501
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
502
|
+
"""Handle button presses."""
|
|
503
|
+
if event.button.id == "change-workdir-btn":
|
|
504
|
+
self.action_change_workdir()
|
|
505
|
+
elif event.button.id == "continue-btn":
|
|
506
|
+
self.action_continue_to_analysis()
|
|
507
|
+
elif event.button.id == "back-btn":
|
|
508
|
+
self.action_back_to_preprocess()
|
|
509
|
+
elif event.button.id == "run-analysis-btn":
|
|
510
|
+
self.action_run_analysis()
|
|
511
|
+
elif event.button.id == "stop-btn":
|
|
512
|
+
self.action_stop()
|
|
513
|
+
|
|
514
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
515
|
+
"""Handle select changes."""
|
|
516
|
+
if event.select.id == "input-mode-select":
|
|
517
|
+
self.state.input_mode = str(event.value)
|
|
518
|
+
self.runner.reset_input()
|
|
519
|
+
elif event.select.id == "preset-select":
|
|
520
|
+
self.state.preset = str(event.value)
|
|
521
|
+
self.apply_preset_params()
|
|
522
|
+
elif event.select.id == "preprocess-method-select":
|
|
523
|
+
self.state.preprocess_method = str(event.value)
|
|
524
|
+
# Enable/disable preprocessing parameters
|
|
525
|
+
is_embed = event.value == "embed_spike_trains"
|
|
526
|
+
self.query_one("#emb-dt", Input).disabled = not is_embed
|
|
527
|
+
self.query_one("#emb-sigma", Input).disabled = not is_embed
|
|
528
|
+
self.query_one("#emb-smooth", Checkbox).disabled = not is_embed
|
|
529
|
+
self.query_one("#emb-speed-filter", Checkbox).disabled = not is_embed
|
|
530
|
+
self.query_one("#emb-min-speed", Input).disabled = not is_embed
|
|
531
|
+
self.query_one("#emb-res", Input).disabled = not is_embed
|
|
532
|
+
elif event.select.id == "analysis-mode-select":
|
|
533
|
+
self.state.analysis_mode = str(event.value)
|
|
534
|
+
self.update_analysis_params_visibility()
|
|
535
|
+
elif event.select.id == "decode-version":
|
|
536
|
+
self.update_decode_controls()
|
|
537
|
+
elif event.select.id == "pc-dim-mode" or event.select.id == "pc-slice-mode":
|
|
538
|
+
self.update_pathcompare_controls()
|
|
539
|
+
elif event.select.id == "coho-dim-mode" or event.select.id == "coho-unfold":
|
|
540
|
+
self.update_cohospace_controls()
|
|
541
|
+
|
|
542
|
+
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
|
543
|
+
"""Handle checkbox changes."""
|
|
544
|
+
if event.checkbox.id == "tda-do-shuffle":
|
|
545
|
+
self.query_one("#tda-num-shuffles", Input).disabled = not event.value
|
|
546
|
+
elif event.checkbox.id == "pc-use-box":
|
|
547
|
+
self.update_pathcompare_controls()
|
|
548
|
+
|
|
549
|
+
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
|
|
550
|
+
"""Handle file selection from tree."""
|
|
551
|
+
selected_path = event.path
|
|
552
|
+
|
|
553
|
+
if self.state.input_mode == "asa" and selected_path.suffix == ".npz":
|
|
554
|
+
self.state.asa_file = relative_path(self.state, selected_path)
|
|
555
|
+
self.runner.reset_input()
|
|
556
|
+
self.log_message(f"Selected ASA file: {self.state.asa_file}")
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
if selected_path.suffix.lower() in {".png", ".jpg", ".jpeg", ".gif", ".bmp"}:
|
|
560
|
+
preview = self.query_one("#result-preview", ImagePreview)
|
|
561
|
+
preview.update_image(selected_path)
|
|
562
|
+
self.log_message(f"Previewing image: {selected_path}")
|
|
563
|
+
|
|
564
|
+
def action_continue_to_analysis(self) -> None:
|
|
565
|
+
"""Continue from preprocessing to analysis page."""
|
|
566
|
+
if self.current_worker and not self.current_worker.is_finished:
|
|
567
|
+
self.log_message("Preprocessing already running. Please wait.")
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
# Validate files
|
|
571
|
+
is_valid, error = validate_files(self.state)
|
|
572
|
+
if not is_valid:
|
|
573
|
+
self.push_screen(ErrorScreen("Validation Error", error))
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
# Collect preprocessing parameters
|
|
577
|
+
if self.state.preprocess_method == "embed_spike_trains":
|
|
578
|
+
try:
|
|
579
|
+
res_val = int(self.query_one("#emb-res", Input).value)
|
|
580
|
+
dt_val = int(self.query_one("#emb-dt", Input).value)
|
|
581
|
+
sigma_val = int(self.query_one("#emb-sigma", Input).value)
|
|
582
|
+
smooth_val = self.query_one("#emb-smooth", Checkbox).value
|
|
583
|
+
speed_filter_val = self.query_one("#emb-speed-filter", Checkbox).value
|
|
584
|
+
min_speed_val = float(self.query_one("#emb-min-speed", Input).value)
|
|
585
|
+
|
|
586
|
+
self.state.preprocess_params = {
|
|
587
|
+
"res": res_val,
|
|
588
|
+
"dt": dt_val,
|
|
589
|
+
"sigma": sigma_val,
|
|
590
|
+
"smooth": smooth_val,
|
|
591
|
+
"speed_filter": speed_filter_val,
|
|
592
|
+
"min_speed": min_speed_val,
|
|
593
|
+
}
|
|
594
|
+
except ValueError as e:
|
|
595
|
+
self.push_screen(ErrorScreen("Parameter Error", f"Invalid parameter value: {e}"))
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
self.log_message("Loading and preprocessing data...")
|
|
599
|
+
self.set_run_status("Status: Preprocessing...", "running")
|
|
600
|
+
self.query_one("#continue-btn", Button).disabled = True
|
|
601
|
+
|
|
602
|
+
# Run preprocessing in worker
|
|
603
|
+
self.current_worker = self.run_worker(
|
|
604
|
+
self.runner.run_preprocessing(
|
|
605
|
+
self.state,
|
|
606
|
+
log_callback=self.log_message,
|
|
607
|
+
progress_callback=self.update_progress,
|
|
608
|
+
),
|
|
609
|
+
name="preprocessing_worker",
|
|
610
|
+
thread=True,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
def action_back_to_preprocess(self) -> None:
|
|
614
|
+
"""Go back to preprocessing page."""
|
|
615
|
+
self.current_page = "preprocess"
|
|
616
|
+
self.query_one("#page-indicator", Label).update("Page: Preprocess")
|
|
617
|
+
self.query_one("#preprocess-controls").remove_class("hidden")
|
|
618
|
+
self.query_one("#analysis-controls").add_class("hidden")
|
|
619
|
+
|
|
620
|
+
# Show/hide appropriate buttons
|
|
621
|
+
self.query_one("#continue-btn").remove_class("hidden")
|
|
622
|
+
self.query_one("#back-btn").add_class("hidden")
|
|
623
|
+
self.query_one("#run-analysis-btn").add_class("hidden")
|
|
624
|
+
self.query_one("#stop-btn").add_class("hidden")
|
|
625
|
+
self.query_one("#stop-btn", Button).disabled = True
|
|
626
|
+
|
|
627
|
+
self.log_message("Returned to preprocessing page")
|
|
628
|
+
|
|
629
|
+
def action_run_analysis(self) -> None:
|
|
630
|
+
"""Run analysis on preprocessed data."""
|
|
631
|
+
if self.current_worker and not self.current_worker.is_finished:
|
|
632
|
+
self.log_message("Another task is already running. Please wait.")
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
if not self.runner.has_preprocessed_data():
|
|
636
|
+
self.push_screen(
|
|
637
|
+
ErrorScreen("Error", "No preprocessed data. Please complete preprocessing first.")
|
|
638
|
+
)
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
try:
|
|
642
|
+
self.collect_analysis_params()
|
|
643
|
+
except ValueError as e:
|
|
644
|
+
self.push_screen(ErrorScreen("Parameter Error", f"Invalid analysis parameter: {e}"))
|
|
645
|
+
return
|
|
646
|
+
|
|
647
|
+
self.log_message(f"Starting {self.state.analysis_mode} analysis...")
|
|
648
|
+
self.set_run_status(f"Status: Running {self.state.analysis_mode}...", "running")
|
|
649
|
+
self.query_one("#run-analysis-btn", Button).disabled = True
|
|
650
|
+
self.query_one("#stop-btn", Button).disabled = False
|
|
651
|
+
|
|
652
|
+
# Run analysis in worker
|
|
653
|
+
self.current_worker = self.run_worker(
|
|
654
|
+
self.runner.run_analysis(
|
|
655
|
+
self.state,
|
|
656
|
+
log_callback=self.log_message,
|
|
657
|
+
progress_callback=self.update_progress,
|
|
658
|
+
),
|
|
659
|
+
name="analysis_worker",
|
|
660
|
+
thread=True,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
def action_run_action(self) -> None:
|
|
664
|
+
"""Run current page action (Continue or Run Analysis)."""
|
|
665
|
+
if self.current_page == "preprocess":
|
|
666
|
+
self.action_continue_to_analysis()
|
|
667
|
+
else:
|
|
668
|
+
self.action_run_analysis()
|
|
669
|
+
|
|
670
|
+
def action_stop(self) -> None:
|
|
671
|
+
"""Request cancellation of the running worker."""
|
|
672
|
+
if not self.current_worker or self.current_worker.is_finished:
|
|
673
|
+
self.log_message("No running task to stop.")
|
|
674
|
+
return
|
|
675
|
+
self.current_worker.cancel()
|
|
676
|
+
self.set_run_status("Status: Cancel requested.", "error")
|
|
677
|
+
self.log_message("Stop requested. Waiting for worker to exit...")
|
|
678
|
+
self.query_one("#stop-btn", Button).disabled = True
|
|
679
|
+
|
|
680
|
+
def log_message(self, message: str) -> None:
|
|
681
|
+
"""Add log message."""
|
|
682
|
+
log_viewer = self.query_one("#log-viewer", LogViewer)
|
|
683
|
+
log_viewer.add_log(message)
|
|
684
|
+
self.append_log_file(message)
|
|
685
|
+
|
|
686
|
+
def _log_file_path(self) -> Path:
|
|
687
|
+
try:
|
|
688
|
+
return self.runner.results_dir(self.state) / "asa_tui.log"
|
|
689
|
+
except Exception:
|
|
690
|
+
return self.state.workdir / "Results" / "asa_tui.log"
|
|
691
|
+
|
|
692
|
+
def append_log_file(self, message: str) -> None:
|
|
693
|
+
"""Append log message to file for easy copying."""
|
|
694
|
+
try:
|
|
695
|
+
path = self._log_file_path()
|
|
696
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
697
|
+
path.open("a", encoding="utf-8").write(f"{message}\n")
|
|
698
|
+
except Exception:
|
|
699
|
+
pass
|
|
700
|
+
|
|
701
|
+
def update_progress(self, percent: int) -> None:
|
|
702
|
+
"""Update progress bar."""
|
|
703
|
+
progress = self.query_one("#progress-bar", ProgressBar)
|
|
704
|
+
progress.update(total=100, progress=percent)
|
|
705
|
+
|
|
706
|
+
def apply_preset_params(self) -> None:
|
|
707
|
+
"""Apply preset defaults to analysis inputs."""
|
|
708
|
+
preset_params = get_preset_params(self.state.preset)
|
|
709
|
+
tda = preset_params.get("tda", {})
|
|
710
|
+
grid = preset_params.get("gridscore", {})
|
|
711
|
+
|
|
712
|
+
if tda:
|
|
713
|
+
self.query_one("#tda-dim", Input).value = str(tda.get("dim", 6))
|
|
714
|
+
self.query_one("#tda-num-times", Input).value = str(tda.get("num_times", 5))
|
|
715
|
+
self.query_one("#tda-active-times", Input).value = str(tda.get("active_times", 15000))
|
|
716
|
+
self.query_one("#tda-k", Input).value = str(tda.get("k", 1000))
|
|
717
|
+
self.query_one("#tda-n-points", Input).value = str(tda.get("n_points", 1200))
|
|
718
|
+
self.query_one("#tda-metric", Select).value = str(tda.get("metric", "cosine"))
|
|
719
|
+
self.query_one("#tda-nbs", Input).value = str(tda.get("nbs", 800))
|
|
720
|
+
self.query_one("#tda-maxdim", Input).value = str(tda.get("maxdim", 1))
|
|
721
|
+
self.query_one("#tda-coeff", Input).value = str(tda.get("coeff", 47))
|
|
722
|
+
self.query_one("#tda-do-shuffle", Checkbox).value = bool(tda.get("do_shuffle", False))
|
|
723
|
+
self.query_one("#tda-num-shuffles", Input).value = str(tda.get("num_shuffles", 1000))
|
|
724
|
+
self.query_one("#tda-num-shuffles", Input).disabled = not self.query_one(
|
|
725
|
+
"#tda-do-shuffle", Checkbox
|
|
726
|
+
).value
|
|
727
|
+
|
|
728
|
+
if grid:
|
|
729
|
+
self.query_one("#gridscore-annulus-inner", Input).value = str(
|
|
730
|
+
grid.get("annulus_inner", 0.3)
|
|
731
|
+
)
|
|
732
|
+
self.query_one("#gridscore-annulus-outer", Input).value = str(
|
|
733
|
+
grid.get("annulus_outer", 0.7)
|
|
734
|
+
)
|
|
735
|
+
self.query_one("#gridscore-bin-size", Input).value = str(grid.get("bin_size", 2.5))
|
|
736
|
+
self.query_one("#gridscore-smooth-sigma", Input).value = str(
|
|
737
|
+
grid.get("smooth_sigma", 2.0)
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
def update_analysis_params_visibility(self) -> None:
|
|
741
|
+
"""Show params for the selected analysis mode."""
|
|
742
|
+
mode = self.state.analysis_mode
|
|
743
|
+
|
|
744
|
+
groups = {
|
|
745
|
+
"analysis-params-tda": mode == "tda",
|
|
746
|
+
"analysis-params-decode": mode in {"cohomap", "pathcompare", "cohospace"},
|
|
747
|
+
"analysis-params-pathcompare": mode == "pathcompare",
|
|
748
|
+
"analysis-params-cohospace": mode == "cohospace",
|
|
749
|
+
"analysis-params-fr": mode == "fr",
|
|
750
|
+
"analysis-params-frm": mode == "frm",
|
|
751
|
+
"analysis-params-gridscore": mode == "gridscore",
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
for group_id, should_show in groups.items():
|
|
755
|
+
group = self.query_one(f"#{group_id}")
|
|
756
|
+
if should_show:
|
|
757
|
+
group.remove_class("hidden")
|
|
758
|
+
else:
|
|
759
|
+
group.add_class("hidden")
|
|
760
|
+
|
|
761
|
+
def update_decode_controls(self) -> None:
|
|
762
|
+
"""Enable/disable decode controls based on decode version."""
|
|
763
|
+
version = str(self.query_one("#decode-version", Select).value)
|
|
764
|
+
is_v0 = version == "v0"
|
|
765
|
+
self.query_one("#decode-real-ground", Checkbox).disabled = not is_v0
|
|
766
|
+
self.query_one("#decode-real-of", Checkbox).disabled = not is_v0
|
|
767
|
+
|
|
768
|
+
def update_pathcompare_controls(self) -> None:
|
|
769
|
+
"""Enable/disable PathCompare controls based on mode."""
|
|
770
|
+
dim_mode = str(self.query_one("#pc-dim-mode", Select).value)
|
|
771
|
+
is_1d = dim_mode == "1d"
|
|
772
|
+
self.query_one("#pc-dim", Input).disabled = not is_1d
|
|
773
|
+
self.query_one("#pc-dim1", Input).disabled = is_1d
|
|
774
|
+
self.query_one("#pc-dim2", Input).disabled = is_1d
|
|
775
|
+
|
|
776
|
+
slice_mode = str(self.query_one("#pc-slice-mode", Select).value)
|
|
777
|
+
is_time = slice_mode == "time"
|
|
778
|
+
self.query_one("#pc-tmin", Input).disabled = not is_time
|
|
779
|
+
self.query_one("#pc-tmax", Input).disabled = not is_time
|
|
780
|
+
self.query_one("#pc-imin", Input).disabled = is_time
|
|
781
|
+
self.query_one("#pc-imax", Input).disabled = is_time
|
|
782
|
+
|
|
783
|
+
use_box = self.query_one("#pc-use-box", Checkbox).value
|
|
784
|
+
self.query_one("#pc-interp-full", Checkbox).disabled = not use_box
|
|
785
|
+
|
|
786
|
+
def update_cohospace_controls(self) -> None:
|
|
787
|
+
"""Enable/disable CohoSpace controls based on mode."""
|
|
788
|
+
dim_mode = str(self.query_one("#coho-dim-mode", Select).value)
|
|
789
|
+
is_1d = dim_mode == "1d"
|
|
790
|
+
self.query_one("#coho-dim", Input).disabled = not is_1d
|
|
791
|
+
self.query_one("#coho-dim1", Input).disabled = is_1d
|
|
792
|
+
self.query_one("#coho-dim2", Input).disabled = is_1d
|
|
793
|
+
|
|
794
|
+
unfold = str(self.query_one("#coho-unfold", Select).value)
|
|
795
|
+
is_skew = unfold == "skew"
|
|
796
|
+
self.query_one("#coho-skew-show-grid", Checkbox).disabled = not is_skew
|
|
797
|
+
self.query_one("#coho-skew-tiles", Input).disabled = not is_skew
|
|
798
|
+
|
|
799
|
+
def _parse_optional_number(self, raw: str, cast) -> int | float | None:
|
|
800
|
+
text = raw.strip()
|
|
801
|
+
if text == "" or text == "-1":
|
|
802
|
+
return None
|
|
803
|
+
return cast(text)
|
|
804
|
+
|
|
805
|
+
def _parse_optional_int(self, raw: str) -> int | None:
|
|
806
|
+
return self._parse_optional_number(raw, int)
|
|
807
|
+
|
|
808
|
+
def _parse_optional_float(self, raw: str) -> float | None:
|
|
809
|
+
return self._parse_optional_number(raw, float)
|
|
810
|
+
|
|
811
|
+
def collect_analysis_params(self) -> None:
|
|
812
|
+
"""Collect analysis parameters from UI into state."""
|
|
813
|
+
params: dict[str, object] = {}
|
|
814
|
+
mode = self.state.analysis_mode
|
|
815
|
+
|
|
816
|
+
if mode == "tda":
|
|
817
|
+
params["dim"] = int(self.query_one("#tda-dim", Input).value)
|
|
818
|
+
params["num_times"] = int(self.query_one("#tda-num-times", Input).value)
|
|
819
|
+
params["active_times"] = int(self.query_one("#tda-active-times", Input).value)
|
|
820
|
+
params["k"] = int(self.query_one("#tda-k", Input).value)
|
|
821
|
+
params["n_points"] = int(self.query_one("#tda-n-points", Input).value)
|
|
822
|
+
params["metric"] = str(self.query_one("#tda-metric", Select).value)
|
|
823
|
+
params["nbs"] = int(self.query_one("#tda-nbs", Input).value)
|
|
824
|
+
params["maxdim"] = int(self.query_one("#tda-maxdim", Input).value)
|
|
825
|
+
params["coeff"] = int(self.query_one("#tda-coeff", Input).value)
|
|
826
|
+
params["do_shuffle"] = self.query_one("#tda-do-shuffle", Checkbox).value
|
|
827
|
+
params["num_shuffles"] = int(self.query_one("#tda-num-shuffles", Input).value)
|
|
828
|
+
params["standardize"] = self.query_one("#tda-standardize", Checkbox).value
|
|
829
|
+
elif mode == "cohomap":
|
|
830
|
+
params["decode_version"] = str(self.query_one("#decode-version", Select).value)
|
|
831
|
+
params["num_circ"] = int(self.query_one("#decode-num-circ", Input).value)
|
|
832
|
+
params["cohomap_subsample"] = int(self.query_one("#cohomap-subsample", Input).value)
|
|
833
|
+
params["real_ground"] = self.query_one("#decode-real-ground", Checkbox).value
|
|
834
|
+
params["real_of"] = self.query_one("#decode-real-of", Checkbox).value
|
|
835
|
+
elif mode == "pathcompare":
|
|
836
|
+
params["decode_version"] = str(self.query_one("#decode-version", Select).value)
|
|
837
|
+
params["num_circ"] = int(self.query_one("#decode-num-circ", Input).value)
|
|
838
|
+
params["real_ground"] = self.query_one("#decode-real-ground", Checkbox).value
|
|
839
|
+
params["real_of"] = self.query_one("#decode-real-of", Checkbox).value
|
|
840
|
+
params["use_box"] = self.query_one("#pc-use-box", Checkbox).value
|
|
841
|
+
params["interp_full"] = self.query_one("#pc-interp-full", Checkbox).value
|
|
842
|
+
params["dim_mode"] = str(self.query_one("#pc-dim-mode", Select).value)
|
|
843
|
+
params["dim"] = int(self.query_one("#pc-dim", Input).value)
|
|
844
|
+
params["dim1"] = int(self.query_one("#pc-dim1", Input).value)
|
|
845
|
+
params["dim2"] = int(self.query_one("#pc-dim2", Input).value)
|
|
846
|
+
params["coords_key"] = self.query_one("#pc-coords-key", Input).value.strip() or None
|
|
847
|
+
params["times_key"] = self.query_one("#pc-times-key", Input).value.strip() or None
|
|
848
|
+
params["slice_mode"] = str(self.query_one("#pc-slice-mode", Select).value)
|
|
849
|
+
params["tmin"] = self._parse_optional_float(self.query_one("#pc-tmin", Input).value)
|
|
850
|
+
params["tmax"] = self._parse_optional_float(self.query_one("#pc-tmax", Input).value)
|
|
851
|
+
params["imin"] = self._parse_optional_int(self.query_one("#pc-imin", Input).value)
|
|
852
|
+
params["imax"] = self._parse_optional_int(self.query_one("#pc-imax", Input).value)
|
|
853
|
+
params["stride"] = int(self.query_one("#pc-stride", Input).value)
|
|
854
|
+
params["angle_scale"] = (
|
|
855
|
+
self.query_one("#pathcompare-angle-scale", Input).value.strip() or "rad"
|
|
856
|
+
)
|
|
857
|
+
elif mode == "cohospace":
|
|
858
|
+
params["decode_version"] = str(self.query_one("#decode-version", Select).value)
|
|
859
|
+
params["num_circ"] = int(self.query_one("#decode-num-circ", Input).value)
|
|
860
|
+
params["real_ground"] = self.query_one("#decode-real-ground", Checkbox).value
|
|
861
|
+
params["real_of"] = self.query_one("#decode-real-of", Checkbox).value
|
|
862
|
+
params["dim_mode"] = str(self.query_one("#coho-dim-mode", Select).value)
|
|
863
|
+
params["dim"] = int(self.query_one("#coho-dim", Input).value)
|
|
864
|
+
params["dim1"] = int(self.query_one("#coho-dim1", Input).value)
|
|
865
|
+
params["dim2"] = int(self.query_one("#coho-dim2", Input).value)
|
|
866
|
+
params["mode"] = str(self.query_one("#coho-mode", Select).value)
|
|
867
|
+
params["top_percent"] = float(self.query_one("#coho-top-percent", Input).value)
|
|
868
|
+
params["view"] = str(self.query_one("#coho-view", Select).value)
|
|
869
|
+
neuron_id_raw = self.query_one("#cohospace-neuron-id", Input).value.strip()
|
|
870
|
+
if neuron_id_raw:
|
|
871
|
+
params["neuron_id"] = int(neuron_id_raw)
|
|
872
|
+
params["subsample"] = int(self.query_one("#coho-subsample", Input).value)
|
|
873
|
+
params["unfold"] = str(self.query_one("#coho-unfold", Select).value)
|
|
874
|
+
params["skew_show_grid"] = self.query_one("#coho-skew-show-grid", Checkbox).value
|
|
875
|
+
params["skew_tiles"] = int(self.query_one("#coho-skew-tiles", Input).value)
|
|
876
|
+
elif mode == "fr":
|
|
877
|
+
n_start = self._parse_optional_int(self.query_one("#fr-neuron-start", Input).value)
|
|
878
|
+
n_end = self._parse_optional_int(self.query_one("#fr-neuron-end", Input).value)
|
|
879
|
+
t_start = self._parse_optional_int(self.query_one("#fr-time-start", Input).value)
|
|
880
|
+
t_end = self._parse_optional_int(self.query_one("#fr-time-end", Input).value)
|
|
881
|
+
params["neuron_range"] = None if n_start is None and n_end is None else (n_start, n_end)
|
|
882
|
+
params["time_range"] = None if t_start is None and t_end is None else (t_start, t_end)
|
|
883
|
+
params["mode"] = str(self.query_one("#fr-mode", Select).value)
|
|
884
|
+
normalize = str(self.query_one("#fr-normalize", Select).value)
|
|
885
|
+
params["normalize"] = None if normalize == "none" else normalize
|
|
886
|
+
elif mode == "frm":
|
|
887
|
+
params["neuron_id"] = int(self.query_one("#frm-neuron-id", Input).value)
|
|
888
|
+
params["bin_size"] = int(self.query_one("#frm-bins", Input).value)
|
|
889
|
+
params["min_occupancy"] = int(self.query_one("#frm-min-occupancy", Input).value)
|
|
890
|
+
params["smoothing"] = self.query_one("#frm-smoothing", Checkbox).value
|
|
891
|
+
params["smooth_sigma"] = float(self.query_one("#frm-smooth-sigma", Input).value)
|
|
892
|
+
params["mode"] = str(self.query_one("#frm-mode", Select).value)
|
|
893
|
+
elif mode == "gridscore":
|
|
894
|
+
params["annulus_inner"] = float(self.query_one("#gridscore-annulus-inner", Input).value)
|
|
895
|
+
params["annulus_outer"] = float(self.query_one("#gridscore-annulus-outer", Input).value)
|
|
896
|
+
params["bin_size"] = float(self.query_one("#gridscore-bin-size", Input).value)
|
|
897
|
+
params["smooth_sigma"] = float(self.query_one("#gridscore-smooth-sigma", Input).value)
|
|
898
|
+
|
|
899
|
+
self.state.analysis_params = params
|
|
900
|
+
|
|
901
|
+
def set_run_status(self, message: str, status_class: str | None = None) -> None:
|
|
902
|
+
"""Update run status label and styling."""
|
|
903
|
+
status = self.query_one("#run-status", Static)
|
|
904
|
+
status.update(message)
|
|
905
|
+
status.remove_class("running", "success", "error")
|
|
906
|
+
if status_class:
|
|
907
|
+
status.add_class(status_class)
|
|
908
|
+
|
|
909
|
+
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
910
|
+
"""Handle worker state changes."""
|
|
911
|
+
if event.worker.name == "preprocessing_worker" and event.worker.is_finished:
|
|
912
|
+
result = event.worker.result
|
|
913
|
+
self.query_one("#continue-btn", Button).disabled = False
|
|
914
|
+
self.query_one("#stop-btn", Button).disabled = True
|
|
915
|
+
|
|
916
|
+
if result.success:
|
|
917
|
+
self.log_message(result.summary)
|
|
918
|
+
self.set_run_status("Status: Preprocessing complete.", "success")
|
|
919
|
+
# Switch to analysis page
|
|
920
|
+
self.current_page = "analysis"
|
|
921
|
+
self.query_one("#page-indicator", Label).update("Page: Analysis")
|
|
922
|
+
self.query_one("#preprocess-controls").add_class("hidden")
|
|
923
|
+
self.query_one("#analysis-controls").remove_class("hidden")
|
|
924
|
+
|
|
925
|
+
# Show/hide appropriate buttons
|
|
926
|
+
self.query_one("#continue-btn").add_class("hidden")
|
|
927
|
+
self.query_one("#back-btn").remove_class("hidden")
|
|
928
|
+
self.query_one("#run-analysis-btn").remove_class("hidden")
|
|
929
|
+
self.query_one("#stop-btn").remove_class("hidden")
|
|
930
|
+
self.query_one("#run-analysis-btn", Button).disabled = False
|
|
931
|
+
|
|
932
|
+
self.log_message("Preprocessing complete. Ready for analysis.")
|
|
933
|
+
tree = self.query_one("#file-tree", DirectoryTree)
|
|
934
|
+
tree.reload()
|
|
935
|
+
else:
|
|
936
|
+
self.set_run_status("Status: Preprocessing failed.", "error")
|
|
937
|
+
self.push_screen(
|
|
938
|
+
ErrorScreen("Preprocessing Error", result.error or "Unknown error")
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
elif event.worker.name == "analysis_worker" and event.worker.is_finished:
|
|
942
|
+
result = event.worker.result
|
|
943
|
+
self.query_one("#run-analysis-btn", Button).disabled = False
|
|
944
|
+
self.query_one("#stop-btn", Button).disabled = True
|
|
945
|
+
|
|
946
|
+
if result.success:
|
|
947
|
+
self.log_message(result.summary)
|
|
948
|
+
self.set_run_status("Status: Analysis complete.", "success")
|
|
949
|
+
self.log_message(f"Artifacts: {list(result.artifacts.keys())}")
|
|
950
|
+
|
|
951
|
+
# Update results tab + preview
|
|
952
|
+
tabs = self.query_one("#results-tabs", TabbedContent)
|
|
953
|
+
tabs.active = "results-tab"
|
|
954
|
+
preview_path = self._select_result_image(result.artifacts)
|
|
955
|
+
if preview_path is not None:
|
|
956
|
+
preview = self.query_one("#result-preview", ImagePreview)
|
|
957
|
+
preview.update_image(preview_path)
|
|
958
|
+
|
|
959
|
+
status = self.query_one("#result-status", Static)
|
|
960
|
+
status.update(f"Analysis completed: {result.summary}")
|
|
961
|
+
tree = self.query_one("#file-tree", DirectoryTree)
|
|
962
|
+
tree.reload()
|
|
963
|
+
else:
|
|
964
|
+
self.set_run_status("Status: Analysis failed.", "error")
|
|
965
|
+
self.push_screen(ErrorScreen("Analysis Error", result.error or "Unknown error"))
|
|
966
|
+
|
|
967
|
+
def action_refresh(self) -> None:
|
|
968
|
+
"""Refresh the UI."""
|
|
969
|
+
self.update_workdir_label()
|
|
970
|
+
tree = self.query_one("#file-tree", DirectoryTree)
|
|
971
|
+
tree.reload()
|
|
972
|
+
|
|
973
|
+
def _select_result_image(self, artifacts: dict[str, Path]) -> Path | None:
|
|
974
|
+
"""Pick a result image to preview, preferring known keys."""
|
|
975
|
+
priority_keys = [
|
|
976
|
+
"barcode",
|
|
977
|
+
"cohomap",
|
|
978
|
+
"path_compare",
|
|
979
|
+
"trajectory",
|
|
980
|
+
"cohospace_trajectory",
|
|
981
|
+
"fr_heatmap",
|
|
982
|
+
"frm",
|
|
983
|
+
"distribution",
|
|
984
|
+
]
|
|
985
|
+
for key in priority_keys:
|
|
986
|
+
path = artifacts.get(key)
|
|
987
|
+
if path and path.exists() and path.suffix.lower() in {".png", ".jpg", ".jpeg"}:
|
|
988
|
+
return path
|
|
989
|
+
for path in artifacts.values():
|
|
990
|
+
if path and path.exists() and path.suffix.lower() in {".png", ".jpg", ".jpeg"}:
|
|
991
|
+
return path
|
|
992
|
+
return None
|
|
993
|
+
|
|
994
|
+
def action_help(self) -> None:
|
|
995
|
+
"""Show help screen."""
|
|
996
|
+
self.push_screen(HelpScreen())
|
|
997
|
+
|
|
998
|
+
def action_quit(self) -> None:
|
|
999
|
+
"""Quit the application."""
|
|
1000
|
+
self.exit()
|