canns 0.13.1__py3-none-any.whl → 0.14.0__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.
Files changed (99) hide show
  1. canns/analyzer/data/__init__.py +5 -1
  2. canns/analyzer/data/asa/__init__.py +27 -12
  3. canns/analyzer/data/asa/cohospace.py +336 -10
  4. canns/analyzer/data/asa/config.py +3 -0
  5. canns/analyzer/data/asa/embedding.py +48 -45
  6. canns/analyzer/data/asa/path.py +104 -2
  7. canns/analyzer/data/asa/plotting.py +88 -19
  8. canns/analyzer/data/asa/tda.py +11 -4
  9. canns/analyzer/data/cell_classification/__init__.py +97 -0
  10. canns/analyzer/data/cell_classification/core/__init__.py +26 -0
  11. canns/analyzer/data/cell_classification/core/grid_cells.py +633 -0
  12. canns/analyzer/data/cell_classification/core/grid_modules_leiden.py +288 -0
  13. canns/analyzer/data/cell_classification/core/head_direction.py +347 -0
  14. canns/analyzer/data/cell_classification/core/spatial_analysis.py +431 -0
  15. canns/analyzer/data/cell_classification/io/__init__.py +5 -0
  16. canns/analyzer/data/cell_classification/io/matlab_loader.py +417 -0
  17. canns/analyzer/data/cell_classification/utils/__init__.py +39 -0
  18. canns/analyzer/data/cell_classification/utils/circular_stats.py +383 -0
  19. canns/analyzer/data/cell_classification/utils/correlation.py +318 -0
  20. canns/analyzer/data/cell_classification/utils/geometry.py +442 -0
  21. canns/analyzer/data/cell_classification/utils/image_processing.py +416 -0
  22. canns/analyzer/data/cell_classification/visualization/__init__.py +19 -0
  23. canns/analyzer/data/cell_classification/visualization/grid_plots.py +292 -0
  24. canns/analyzer/data/cell_classification/visualization/hd_plots.py +200 -0
  25. canns/analyzer/metrics/__init__.py +2 -1
  26. canns/analyzer/visualization/core/config.py +46 -4
  27. canns/data/__init__.py +6 -1
  28. canns/data/datasets.py +154 -1
  29. canns/data/loaders.py +37 -0
  30. canns/pipeline/__init__.py +13 -9
  31. canns/pipeline/__main__.py +6 -0
  32. canns/pipeline/asa/runner.py +105 -41
  33. canns/pipeline/asa_gui/__init__.py +68 -0
  34. canns/pipeline/asa_gui/__main__.py +6 -0
  35. canns/pipeline/asa_gui/analysis_modes/__init__.py +42 -0
  36. canns/pipeline/asa_gui/analysis_modes/base.py +39 -0
  37. canns/pipeline/asa_gui/analysis_modes/batch_mode.py +21 -0
  38. canns/pipeline/asa_gui/analysis_modes/cohomap_mode.py +56 -0
  39. canns/pipeline/asa_gui/analysis_modes/cohospace_mode.py +194 -0
  40. canns/pipeline/asa_gui/analysis_modes/decode_mode.py +52 -0
  41. canns/pipeline/asa_gui/analysis_modes/fr_mode.py +81 -0
  42. canns/pipeline/asa_gui/analysis_modes/frm_mode.py +92 -0
  43. canns/pipeline/asa_gui/analysis_modes/gridscore_mode.py +123 -0
  44. canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py +199 -0
  45. canns/pipeline/asa_gui/analysis_modes/tda_mode.py +112 -0
  46. canns/pipeline/asa_gui/app.py +29 -0
  47. canns/pipeline/asa_gui/controllers/__init__.py +6 -0
  48. canns/pipeline/asa_gui/controllers/analysis_controller.py +59 -0
  49. canns/pipeline/asa_gui/controllers/preprocess_controller.py +89 -0
  50. canns/pipeline/asa_gui/core/__init__.py +15 -0
  51. canns/pipeline/asa_gui/core/cache.py +14 -0
  52. canns/pipeline/asa_gui/core/runner.py +1936 -0
  53. canns/pipeline/asa_gui/core/state.py +324 -0
  54. canns/pipeline/asa_gui/core/worker.py +260 -0
  55. canns/pipeline/asa_gui/main_window.py +184 -0
  56. canns/pipeline/asa_gui/models/__init__.py +7 -0
  57. canns/pipeline/asa_gui/models/config.py +14 -0
  58. canns/pipeline/asa_gui/models/job.py +31 -0
  59. canns/pipeline/asa_gui/models/presets.py +21 -0
  60. canns/pipeline/asa_gui/resources/__init__.py +16 -0
  61. canns/pipeline/asa_gui/resources/dark.qss +167 -0
  62. canns/pipeline/asa_gui/resources/light.qss +163 -0
  63. canns/pipeline/asa_gui/resources/styles.qss +130 -0
  64. canns/pipeline/asa_gui/utils/__init__.py +1 -0
  65. canns/pipeline/asa_gui/utils/formatters.py +15 -0
  66. canns/pipeline/asa_gui/utils/io_adapters.py +40 -0
  67. canns/pipeline/asa_gui/utils/validators.py +41 -0
  68. canns/pipeline/asa_gui/views/__init__.py +1 -0
  69. canns/pipeline/asa_gui/views/help_content.py +171 -0
  70. canns/pipeline/asa_gui/views/pages/__init__.py +6 -0
  71. canns/pipeline/asa_gui/views/pages/analysis_page.py +565 -0
  72. canns/pipeline/asa_gui/views/pages/preprocess_page.py +492 -0
  73. canns/pipeline/asa_gui/views/panels/__init__.py +1 -0
  74. canns/pipeline/asa_gui/views/widgets/__init__.py +21 -0
  75. canns/pipeline/asa_gui/views/widgets/artifacts_tab.py +44 -0
  76. canns/pipeline/asa_gui/views/widgets/drop_zone.py +80 -0
  77. canns/pipeline/asa_gui/views/widgets/file_list.py +27 -0
  78. canns/pipeline/asa_gui/views/widgets/gridscore_tab.py +308 -0
  79. canns/pipeline/asa_gui/views/widgets/help_dialog.py +27 -0
  80. canns/pipeline/asa_gui/views/widgets/image_tab.py +50 -0
  81. canns/pipeline/asa_gui/views/widgets/image_viewer.py +97 -0
  82. canns/pipeline/asa_gui/views/widgets/log_box.py +16 -0
  83. canns/pipeline/asa_gui/views/widgets/pathcompare_tab.py +200 -0
  84. canns/pipeline/asa_gui/views/widgets/popup_combo.py +25 -0
  85. canns/pipeline/gallery/__init__.py +15 -5
  86. canns/pipeline/gallery/__main__.py +11 -0
  87. canns/pipeline/gallery/app.py +705 -0
  88. canns/pipeline/gallery/runner.py +790 -0
  89. canns/pipeline/gallery/state.py +51 -0
  90. canns/pipeline/gallery/styles.tcss +123 -0
  91. canns/pipeline/launcher.py +81 -0
  92. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/METADATA +11 -1
  93. canns-0.14.0.dist-info/RECORD +163 -0
  94. canns-0.14.0.dist-info/entry_points.txt +5 -0
  95. canns/pipeline/_base.py +0 -50
  96. canns-0.13.1.dist-info/RECORD +0 -89
  97. canns-0.13.1.dist-info/entry_points.txt +0 -3
  98. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/WHEEL +0 -0
  99. {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,492 @@
1
+ """Preprocess page for ASA GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from PySide6.QtCore import QSettings, Qt, Signal
8
+ from PySide6.QtGui import QColor
9
+ from PySide6.QtWidgets import (
10
+ QCheckBox,
11
+ QDoubleSpinBox,
12
+ QFileDialog,
13
+ QFormLayout,
14
+ QGraphicsDropShadowEffect,
15
+ QGroupBox,
16
+ QHBoxLayout,
17
+ QLabel,
18
+ QProgressBar,
19
+ QPushButton,
20
+ QSpinBox,
21
+ QSplitter,
22
+ QVBoxLayout,
23
+ QWidget,
24
+ )
25
+
26
+ from ...controllers import PreprocessController
27
+ from ...core import WorkerManager
28
+ from ..help_content import preprocess_help_markdown
29
+ from ..widgets.drop_zone import DropZone
30
+ from ..widgets.help_dialog import show_help_dialog
31
+ from ..widgets.log_box import LogBox
32
+ from ..widgets.popup_combo import PopupComboBox
33
+
34
+
35
+ class PreprocessPage(QWidget):
36
+ """Page for loading inputs and running preprocessing."""
37
+
38
+ preprocess_completed = Signal()
39
+
40
+ def __init__(
41
+ self,
42
+ controller: PreprocessController,
43
+ worker_manager: WorkerManager,
44
+ parent=None,
45
+ ) -> None:
46
+ super().__init__(parent)
47
+ self._controller = controller
48
+ self._workers = worker_manager
49
+ self._lang = "en"
50
+ self._build_ui()
51
+
52
+ def _build_ui(self) -> None:
53
+ root = QVBoxLayout(self)
54
+
55
+ title_row = QHBoxLayout()
56
+ self.title_label = QLabel("Preprocess")
57
+ self.title_label.setAlignment(Qt.AlignLeft)
58
+ self.title_label.setStyleSheet("font-size: 18px; font-weight: 600;")
59
+ title_row.addWidget(self.title_label)
60
+ title_row.addStretch(1)
61
+ self.help_btn = QPushButton("Help")
62
+ self.help_btn.setToolTip("Show preprocess parameter guide.")
63
+ self.help_btn.clicked.connect(self._show_help)
64
+ title_row.addWidget(self.help_btn)
65
+ root.addLayout(title_row)
66
+
67
+ content_split = QSplitter(Qt.Vertical)
68
+ root.addWidget(content_split, 1)
69
+
70
+ top_wrap = QWidget()
71
+ top_layout = QVBoxLayout(top_wrap)
72
+
73
+ # Input group
74
+ input_group = QGroupBox("Input")
75
+ input_group.setObjectName("card")
76
+ input_layout = QVBoxLayout(input_group)
77
+ self.input_group = input_group
78
+
79
+ top_row = QHBoxLayout()
80
+ self.input_mode = PopupComboBox()
81
+ self.input_mode.addItem("ASA (.npz)", userData="asa")
82
+ self.input_mode.setEnabled(False)
83
+ self.input_mode.setToolTip("Only ASA .npz input is supported in this GUI.")
84
+
85
+ self.preset = PopupComboBox()
86
+ self.preset.addItems(["grid", "hd", "none"])
87
+ self.preset.setToolTip("Preset hints apply to analysis mode defaults.")
88
+
89
+ self.label_mode = QLabel("Mode")
90
+ top_row.addWidget(self.label_mode)
91
+ top_row.addWidget(self.input_mode)
92
+ top_row.addSpacing(16)
93
+ self.label_preset = QLabel("Preset")
94
+ top_row.addWidget(self.label_preset)
95
+ top_row.addWidget(self.preset)
96
+ top_row.addStretch(1)
97
+ input_layout.addLayout(top_row)
98
+
99
+ # ASA input
100
+ self.asa_zone = DropZone("ASA file", "Drop a .npz with spike/x/y/t")
101
+ self.asa_browse = QPushButton("Browse")
102
+ self.asa_browse.clicked.connect(self._browse_asa)
103
+ asa_row = QHBoxLayout()
104
+ asa_row.addWidget(self.asa_zone, 1)
105
+ asa_row.addWidget(self.asa_browse)
106
+ input_layout.addLayout(asa_row)
107
+ self.asa_hint = QLabel("Expected keys: spike, x, y, t")
108
+ self.asa_hint.setObjectName("muted")
109
+ input_layout.addWidget(self.asa_hint)
110
+
111
+ # Neuron + Trajectory input
112
+ self.neuron_zone = DropZone("Neuron file", "Drop neuron .npy or .npz")
113
+ self.neuron_browse = QPushButton("Browse")
114
+ self.neuron_browse.clicked.connect(self._browse_neuron)
115
+ neuron_row = QHBoxLayout()
116
+ neuron_row.addWidget(self.neuron_zone, 1)
117
+ neuron_row.addWidget(self.neuron_browse)
118
+
119
+ self.traj_zone = DropZone("Trajectory file", "Drop trajectory .npy or .npz")
120
+ self.traj_browse = QPushButton("Browse")
121
+ self.traj_browse.clicked.connect(self._browse_traj)
122
+ traj_row = QHBoxLayout()
123
+ traj_row.addWidget(self.traj_zone, 1)
124
+ traj_row.addWidget(self.traj_browse)
125
+
126
+ input_layout.addLayout(neuron_row)
127
+ input_layout.addLayout(traj_row)
128
+
129
+ top_layout.addWidget(input_group)
130
+
131
+ # Preprocess group
132
+ preprocess_group = QGroupBox("Preprocess")
133
+ preprocess_group.setObjectName("card")
134
+ preprocess_layout = QFormLayout(preprocess_group)
135
+ self.preprocess_group = preprocess_group
136
+
137
+ self.preprocess_method = PopupComboBox()
138
+ self.preprocess_method.addItem("None", userData="none")
139
+ self.preprocess_method.addItem("Embed spike trains", userData="embed_spike_trains")
140
+ self.preprocess_method.setToolTip("Embedding builds a dense spike matrix for TDA/FR.")
141
+ self.preprocess_method.currentIndexChanged.connect(self._toggle_embed_params)
142
+
143
+ preprocess_layout.addRow("Method", self.preprocess_method)
144
+ self.label_method = preprocess_layout.labelForField(self.preprocess_method)
145
+
146
+ self.embed_params = QWidget()
147
+ embed_form = QFormLayout(self.embed_params)
148
+
149
+ defaults = self._embedding_defaults()
150
+
151
+ self.embed_res = QSpinBox()
152
+ self.embed_res.setRange(1, 1_000_000)
153
+ self.embed_res.setValue(int(defaults["res"]))
154
+ self.embed_res.setToolTip("时间分箱分辨率(与 t 的单位一致)。")
155
+
156
+ self.embed_dt = QSpinBox()
157
+ self.embed_dt.setRange(1, 1_000_000)
158
+ self.embed_dt.setValue(int(defaults["dt"]))
159
+ self.embed_dt.setToolTip("时间步长(与 t 的单位一致)。")
160
+
161
+ self.embed_sigma = QSpinBox()
162
+ self.embed_sigma.setRange(1, 1_000_000)
163
+ self.embed_sigma.setValue(int(defaults["sigma"]))
164
+ self.embed_sigma.setToolTip("高斯平滑尺度,越大越平滑。")
165
+
166
+ self.embed_smooth = QCheckBox()
167
+ self.embed_smooth.setChecked(bool(defaults["smooth"]))
168
+ self.embed_smooth.setToolTip("是否对嵌入后的矩阵做平滑。")
169
+
170
+ self.embed_speed_filter = QCheckBox()
171
+ self.embed_speed_filter.setChecked(bool(defaults["speed_filter"]))
172
+ self.embed_speed_filter.setToolTip("过滤低速时间点(常见于 grid 数据)。")
173
+
174
+ self.embed_min_speed = QDoubleSpinBox()
175
+ self.embed_min_speed.setRange(0.0, 1000.0)
176
+ self.embed_min_speed.setDecimals(2)
177
+ self.embed_min_speed.setValue(float(defaults["min_speed"]))
178
+ self.embed_min_speed.setToolTip("速度阈值(与 t/x/y 的单位一致)。")
179
+
180
+ embed_form.addRow("res", self.embed_res)
181
+ self.label_res = embed_form.labelForField(self.embed_res)
182
+ embed_form.addRow("dt", self.embed_dt)
183
+ self.label_dt = embed_form.labelForField(self.embed_dt)
184
+ embed_form.addRow("sigma", self.embed_sigma)
185
+ self.label_sigma = embed_form.labelForField(self.embed_sigma)
186
+ embed_form.addRow("smooth", self.embed_smooth)
187
+ self.label_smooth = embed_form.labelForField(self.embed_smooth)
188
+ embed_form.addRow("speed_filter", self.embed_speed_filter)
189
+ self.label_speed = embed_form.labelForField(self.embed_speed_filter)
190
+ embed_form.addRow("min_speed", self.embed_min_speed)
191
+ self.label_min_speed = embed_form.labelForField(self.embed_min_speed)
192
+
193
+ preprocess_layout.addRow(self.embed_params)
194
+
195
+ top_layout.addWidget(preprocess_group)
196
+
197
+ # Pre-classification (placeholder)
198
+ preclass_group = QGroupBox("Pre-classification")
199
+ preclass_group.setObjectName("card")
200
+ preclass_form = QFormLayout(preclass_group)
201
+ self.preclass_group = preclass_group
202
+ self.preclass = PopupComboBox()
203
+ self.preclass.addItems(["none", "grid", "hd"])
204
+ self.preclass.setCurrentText("none")
205
+ preclass_form.addRow("Preclass", self.preclass)
206
+ self.label_preclass = preclass_form.labelForField(self.preclass)
207
+ top_layout.addWidget(preclass_group)
208
+
209
+ # Controls
210
+ control_row = QHBoxLayout()
211
+ self.run_btn = QPushButton("Run Preprocess")
212
+ self.run_btn.setObjectName("btn_run")
213
+ self.run_btn.clicked.connect(self._run_preprocess)
214
+ self.stop_btn = QPushButton("Stop")
215
+ self.stop_btn.setObjectName("btn_stop")
216
+ self.stop_btn.clicked.connect(self._stop_preprocess)
217
+ self.stop_btn.setEnabled(False)
218
+ self.progress = QProgressBar()
219
+ self.progress.setRange(0, 100)
220
+ self.progress.setValue(0)
221
+
222
+ control_row.addWidget(self.run_btn)
223
+ control_row.addWidget(self.stop_btn)
224
+ control_row.addWidget(self.progress, 1)
225
+ top_layout.addLayout(control_row)
226
+
227
+ log_wrap = QWidget()
228
+ log_layout = QVBoxLayout(log_wrap)
229
+ self.logs_label = QLabel("Logs")
230
+ log_layout.addWidget(self.logs_label)
231
+ self.log_box = LogBox()
232
+ log_layout.addWidget(self.log_box, 1)
233
+
234
+ content_split.addWidget(top_wrap)
235
+ content_split.addWidget(log_wrap)
236
+ content_split.setStretchFactor(0, 3)
237
+ content_split.setStretchFactor(1, 1)
238
+
239
+ self.input_mode.currentIndexChanged.connect(self._toggle_input_mode)
240
+ self.asa_zone.fileDropped.connect(lambda path: self.asa_zone.set_path(path))
241
+ self.neuron_zone.fileDropped.connect(lambda path: self.neuron_zone.set_path(path))
242
+ self.traj_zone.fileDropped.connect(lambda path: self.traj_zone.set_path(path))
243
+ self.asa_zone.fileDropped.connect(lambda _: self._update_run_enabled())
244
+ self.asa_browse.clicked.connect(self._update_run_enabled)
245
+
246
+ self._toggle_input_mode()
247
+ self._toggle_embed_params()
248
+ self._update_run_enabled()
249
+ self._apply_card_effects([input_group, preprocess_group, preclass_group])
250
+ self.apply_language(str(QSettings("canns", "asa_gui").value("lang", "en")))
251
+
252
+ def _apply_card_effects(self, widgets: list[QWidget]) -> None:
253
+ for widget in widgets:
254
+ effect = QGraphicsDropShadowEffect(self)
255
+ effect.setBlurRadius(18)
256
+ effect.setOffset(0, 3)
257
+ effect.setColor(QColor(0, 0, 0, 40))
258
+ widget.setGraphicsEffect(effect)
259
+
260
+ def apply_language(self, lang: str) -> None:
261
+ self._lang = str(lang or "en")
262
+ is_zh = self._lang.lower().startswith("zh")
263
+ self.title_label.setText("预处理" if is_zh else "Preprocess")
264
+ self.help_btn.setText("帮助" if is_zh else "Help")
265
+ self.help_btn.setToolTip("查看参数说明" if is_zh else "Show preprocess parameter guide.")
266
+
267
+ self.input_group.setTitle("输入" if is_zh else "Input")
268
+ self.preprocess_group.setTitle("预处理" if is_zh else "Preprocess")
269
+ self.preclass_group.setTitle("预分类" if is_zh else "Pre-classification")
270
+
271
+ self.label_mode.setText("模式" if is_zh else "Mode")
272
+ self.label_preset.setText("预设" if is_zh else "Preset")
273
+ if self.label_method is not None:
274
+ self.label_method.setText("方法" if is_zh else "Method")
275
+ if self.label_preclass is not None:
276
+ self.label_preclass.setText("预分类" if is_zh else "Preclass")
277
+
278
+ if self.label_res is not None:
279
+ self.label_res.setText("res")
280
+ if self.label_dt is not None:
281
+ self.label_dt.setText("dt")
282
+ if self.label_sigma is not None:
283
+ self.label_sigma.setText("sigma")
284
+ if self.label_smooth is not None:
285
+ self.label_smooth.setText("smooth")
286
+ if self.label_speed is not None:
287
+ self.label_speed.setText("speed_filter")
288
+ if self.label_min_speed is not None:
289
+ self.label_min_speed.setText("min_speed")
290
+
291
+ self.asa_zone.set_title("ASA 文件" if is_zh else "ASA file")
292
+ self.asa_zone.set_hint(
293
+ "拖入含 spike/x/y/t 的 .npz" if is_zh else "Drop a .npz with spike/x/y/t"
294
+ )
295
+ self.asa_zone.set_empty_text("未选择文件" if is_zh else "No file")
296
+ self.asa_hint.setText(
297
+ "需要字段:spike, x, y, t" if is_zh else "Expected keys: spike, x, y, t"
298
+ )
299
+
300
+ self.neuron_zone.set_title("Neuron 文件" if is_zh else "Neuron file")
301
+ self.neuron_zone.set_hint(
302
+ "拖入 neuron .npy 或 .npz" if is_zh else "Drop neuron .npy or .npz"
303
+ )
304
+ self.neuron_zone.set_empty_text("未选择文件" if is_zh else "No file")
305
+
306
+ self.traj_zone.set_title("Trajectory 文件" if is_zh else "Trajectory file")
307
+ self.traj_zone.set_hint(
308
+ "拖入 trajectory .npy 或 .npz" if is_zh else "Drop trajectory .npy or .npz"
309
+ )
310
+ self.traj_zone.set_empty_text("未选择文件" if is_zh else "No file")
311
+
312
+ self.asa_browse.setText("浏览" if is_zh else "Browse")
313
+ self.neuron_browse.setText("浏览" if is_zh else "Browse")
314
+ self.traj_browse.setText("浏览" if is_zh else "Browse")
315
+
316
+ self.run_btn.setText("运行预处理" if is_zh else "Run Preprocess")
317
+ self.stop_btn.setText("停止" if is_zh else "Stop")
318
+ self.logs_label.setText("日志" if is_zh else "Logs")
319
+
320
+ def _show_help(self) -> None:
321
+ lang = str(QSettings("canns", "asa_gui").value("lang", "en"))
322
+ title = (
323
+ "Preprocess Guide" if not str(lang).lower().startswith("zh") else "Preprocess 参数说明"
324
+ )
325
+ show_help_dialog(self, title, preprocess_help_markdown(lang=lang))
326
+
327
+ def _embedding_defaults(self) -> dict:
328
+ try:
329
+ from canns.analyzer.data.asa import SpikeEmbeddingConfig
330
+
331
+ cfg = SpikeEmbeddingConfig()
332
+ return {
333
+ "res": cfg.res,
334
+ "dt": cfg.dt,
335
+ "sigma": cfg.sigma,
336
+ "smooth": cfg.smooth,
337
+ "speed_filter": cfg.speed_filter,
338
+ "min_speed": cfg.min_speed,
339
+ }
340
+ except Exception:
341
+ return {
342
+ "res": 100000,
343
+ "dt": 1000,
344
+ "sigma": 5000,
345
+ "smooth": True,
346
+ "speed_filter": True,
347
+ "min_speed": 2.5,
348
+ }
349
+
350
+ def _toggle_input_mode(self) -> None:
351
+ mode = self.input_mode.currentData() or "asa"
352
+ use_asa = mode == "asa"
353
+ self.asa_zone.setVisible(use_asa)
354
+ self.asa_browse.setVisible(use_asa)
355
+ self.neuron_zone.setVisible(not use_asa)
356
+ self.neuron_browse.setVisible(not use_asa)
357
+ self.traj_zone.setVisible(not use_asa)
358
+ self.traj_browse.setVisible(not use_asa)
359
+
360
+ def _toggle_embed_params(self) -> None:
361
+ method = self.preprocess_method.currentData() or "none"
362
+ self.embed_params.setVisible(method == "embed_spike_trains")
363
+
364
+ def _browse_asa(self) -> None:
365
+ path, _ = QFileDialog.getOpenFileName(self, "Select ASA file", str(Path.cwd()))
366
+ if path:
367
+ self.asa_zone.set_path(path)
368
+ self._update_run_enabled()
369
+
370
+ def _browse_neuron(self) -> None:
371
+ path, _ = QFileDialog.getOpenFileName(self, "Select neuron file", str(Path.cwd()))
372
+ if path:
373
+ self.neuron_zone.set_path(path)
374
+
375
+ def _browse_traj(self) -> None:
376
+ path, _ = QFileDialog.getOpenFileName(self, "Select trajectory file", str(Path.cwd()))
377
+ if path:
378
+ self.traj_zone.set_path(path)
379
+
380
+ def _collect_params(self) -> dict:
381
+ preprocess_method = self.preprocess_method.currentData() or "none"
382
+ params = {}
383
+ if preprocess_method == "embed_spike_trains":
384
+ params = {
385
+ "res": int(self.embed_res.value()),
386
+ "dt": int(self.embed_dt.value()),
387
+ "sigma": int(self.embed_sigma.value()),
388
+ "smooth": bool(self.embed_smooth.isChecked()),
389
+ "speed_filter": bool(self.embed_speed_filter.isChecked()),
390
+ "min_speed": float(self.embed_min_speed.value()),
391
+ }
392
+ return params
393
+
394
+ def _run_preprocess(self) -> None:
395
+ if self._workers.is_running():
396
+ self.log_box.log("A task is already running.")
397
+ return
398
+
399
+ input_mode = self.input_mode.currentData() or "asa"
400
+ preset = self.preset.currentText()
401
+ preprocess_method = self.preprocess_method.currentData() or "none"
402
+
403
+ asa_file = self.asa_zone.path() if input_mode == "asa" else None
404
+ neuron_file = self.neuron_zone.path() if input_mode != "asa" else None
405
+ traj_file = self.traj_zone.path() if input_mode != "asa" else None
406
+
407
+ if not self._validate_inputs(asa_file):
408
+ return
409
+
410
+ self._controller.update_inputs(
411
+ input_mode=input_mode,
412
+ preset=preset,
413
+ asa_file=asa_file,
414
+ neuron_file=neuron_file,
415
+ traj_file=traj_file,
416
+ preprocess_method=preprocess_method,
417
+ preprocess_params=self._collect_params(),
418
+ preclass=str(self.preclass.currentText()),
419
+ preclass_params={},
420
+ )
421
+
422
+ self.progress.setValue(0)
423
+ self.stop_btn.setEnabled(True)
424
+ self.run_btn.setEnabled(False)
425
+ self.log_box.log("Starting preprocessing...")
426
+
427
+ def _on_log(msg: str) -> None:
428
+ self.log_box.log(msg)
429
+
430
+ def _on_progress(pct: int) -> None:
431
+ self.progress.setValue(pct)
432
+
433
+ def _on_finished(result) -> None:
434
+ if hasattr(result, "success") and not result.success:
435
+ self._controller.mark_idle()
436
+ self.log_box.log(result.error or "Preprocessing failed")
437
+ self.run_btn.setEnabled(True)
438
+ self.stop_btn.setEnabled(False)
439
+ return
440
+ self._controller.finalize_preprocess()
441
+ self.log_box.log(result.summary)
442
+ self.run_btn.setEnabled(True)
443
+ self.stop_btn.setEnabled(False)
444
+ self.preprocess_completed.emit()
445
+
446
+ def _on_error(msg: str) -> None:
447
+ self._controller.mark_idle()
448
+ self.log_box.log(f"Error: {msg}")
449
+ self.run_btn.setEnabled(True)
450
+ self.stop_btn.setEnabled(False)
451
+
452
+ def _on_cleanup() -> None:
453
+ self._controller.mark_idle()
454
+
455
+ self._controller.run_preprocess(
456
+ worker_manager=self._workers,
457
+ on_log=_on_log,
458
+ on_progress=_on_progress,
459
+ on_finished=_on_finished,
460
+ on_error=_on_error,
461
+ on_cleanup=_on_cleanup,
462
+ )
463
+
464
+ def _stop_preprocess(self) -> None:
465
+ if self._workers.is_running():
466
+ self._workers.request_cancel()
467
+ self.log_box.log("Cancel requested.")
468
+
469
+ def _validate_inputs(self, asa_file: str | None) -> bool:
470
+ if not asa_file:
471
+ self.log_box.log("Please select an ASA .npz file before running.")
472
+ return False
473
+ path = Path(asa_file)
474
+ if not path.exists():
475
+ self.log_box.log(f"ASA file not found: {path}")
476
+ return False
477
+ if path.suffix.lower() != ".npz":
478
+ self.log_box.log("ASA input must be a .npz file.")
479
+ return False
480
+ return True
481
+
482
+ def _update_run_enabled(self) -> None:
483
+ asa_file = self.asa_zone.path()
484
+ valid = False
485
+ if asa_file:
486
+ path = Path(asa_file)
487
+ valid = path.exists() and path.suffix.lower() == ".npz"
488
+ self.run_btn.setEnabled(True)
489
+ if valid:
490
+ self.run_btn.setToolTip("")
491
+ else:
492
+ self.run_btn.setToolTip("Select a valid ASA .npz file to run preprocessing.")
@@ -0,0 +1 @@
1
+ """Panel components for ASA GUI."""
@@ -0,0 +1,21 @@
1
+ """Reusable widgets for ASA GUI."""
2
+
3
+ from .artifacts_tab import ArtifactsTab
4
+ from .drop_zone import DropZone
5
+ from .file_list import FileList
6
+ from .gridscore_tab import GridScoreTab
7
+ from .image_tab import ImageTab
8
+ from .image_viewer import ImageViewer
9
+ from .log_box import LogBox
10
+ from .pathcompare_tab import PathCompareTab
11
+
12
+ __all__ = [
13
+ "ArtifactsTab",
14
+ "DropZone",
15
+ "FileList",
16
+ "GridScoreTab",
17
+ "ImageTab",
18
+ "ImageViewer",
19
+ "LogBox",
20
+ "PathCompareTab",
21
+ ]
@@ -0,0 +1,44 @@
1
+ """Artifacts tab with file list and quick actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
8
+
9
+ from .file_list import FileList
10
+
11
+
12
+ class ArtifactsTab(QWidget):
13
+ def __init__(self, parent=None) -> None:
14
+ super().__init__(parent)
15
+ self._last_dir: Path | None = None
16
+
17
+ root = QVBoxLayout(self)
18
+ self.files_list = FileList()
19
+ root.addWidget(self.files_list, 1)
20
+
21
+ actions = QHBoxLayout()
22
+ self.btn_open_folder = QPushButton("Open Folder")
23
+ self.btn_open_folder.setEnabled(False)
24
+ self.btn_open_folder.clicked.connect(self._open_folder)
25
+ actions.addWidget(self.btn_open_folder)
26
+ actions.addStretch(1)
27
+ root.addLayout(actions)
28
+
29
+ def set_artifacts(self, artifacts: dict) -> None:
30
+ self.files_list.clear()
31
+ self._last_dir = None
32
+ for _, path in artifacts.items():
33
+ self.files_list.addItem(f"{_}: {path}")
34
+ if self._last_dir is None:
35
+ self._last_dir = Path(path).parent
36
+ self.btn_open_folder.setEnabled(self._last_dir is not None)
37
+
38
+ def _open_folder(self) -> None:
39
+ if self._last_dir is None:
40
+ return
41
+ from PySide6.QtCore import QUrl
42
+ from PySide6.QtGui import QDesktopServices
43
+
44
+ QDesktopServices.openUrl(QUrl.fromLocalFile(str(self._last_dir)))
@@ -0,0 +1,80 @@
1
+ """Drag-and-drop file input widget."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from PySide6.QtCore import Qt, Signal
6
+ from PySide6.QtWidgets import QFrame, QLabel, QVBoxLayout
7
+
8
+
9
+ class DropZone(QFrame):
10
+ """Simple drag-and-drop target for file paths."""
11
+
12
+ fileDropped = Signal(str)
13
+
14
+ def __init__(self, title: str, hint: str = "", parent=None) -> None:
15
+ super().__init__(parent)
16
+ self.setAcceptDrops(True)
17
+ self.setObjectName("DropZone")
18
+ self.setFrameShape(QFrame.StyledPanel)
19
+ self.setMinimumHeight(110)
20
+
21
+ self._title_text = title
22
+ self._hint_text = hint
23
+ self._empty_text = "No file"
24
+
25
+ self._title = QLabel(f"<b>{title}</b>")
26
+ self._hint = QLabel(hint)
27
+ self._hint.setObjectName("muted")
28
+
29
+ self._path_label = QLabel(self._empty_text)
30
+ self._path_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
31
+ self._current_path: str | None = None
32
+
33
+ layout = QVBoxLayout(self)
34
+ layout.addWidget(self._title)
35
+ layout.addWidget(self._hint)
36
+ layout.addWidget(self._path_label)
37
+
38
+ def set_path(self, path: str) -> None:
39
+ self._path_label.setText(path)
40
+ self._current_path = path
41
+
42
+ def set_title(self, title: str) -> None:
43
+ self._title_text = title
44
+ self._title.setText(f"<b>{title}</b>")
45
+
46
+ def set_hint(self, hint: str) -> None:
47
+ self._hint_text = hint
48
+ self._hint.setText(hint)
49
+
50
+ def set_empty_text(self, text: str) -> None:
51
+ self._empty_text = text
52
+ if not self._current_path:
53
+ self._path_label.setText(text)
54
+
55
+ def path(self) -> str | None:
56
+ return self._current_path
57
+
58
+ def dragEnterEvent(self, event) -> None: # noqa: N802 - Qt naming
59
+ if event.mimeData().hasUrls():
60
+ self.setProperty("drag", True)
61
+ self.style().unpolish(self)
62
+ self.style().polish(self)
63
+ event.acceptProposedAction()
64
+
65
+ def dragLeaveEvent(self, event) -> None: # noqa: N802 - Qt naming
66
+ self.setProperty("drag", False)
67
+ self.style().unpolish(self)
68
+ self.style().polish(self)
69
+
70
+ def dropEvent(self, event) -> None: # noqa: N802 - Qt naming
71
+ self.setProperty("drag", False)
72
+ self.style().unpolish(self)
73
+ self.style().polish(self)
74
+
75
+ urls = event.mimeData().urls()
76
+ if not urls:
77
+ return
78
+ path = urls[0].toLocalFile()
79
+ self.set_path(path)
80
+ self.fileDropped.emit(path)
@@ -0,0 +1,27 @@
1
+ """Artifacts list widget with open actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from PySide6.QtCore import QUrl
8
+ from PySide6.QtGui import QDesktopServices
9
+ from PySide6.QtWidgets import QListWidget
10
+
11
+
12
+ class FileList(QListWidget):
13
+ def __init__(self, parent=None) -> None:
14
+ super().__init__(parent)
15
+ self.itemDoubleClicked.connect(self._open_item)
16
+
17
+ def _open_item(self) -> None:
18
+ item = self.currentItem()
19
+ if item is None:
20
+ return
21
+ text = item.text()
22
+ if ": " not in text:
23
+ return
24
+ _, path_str = text.split(": ", 1)
25
+ path = Path(path_str)
26
+ if path.exists():
27
+ QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))