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.
- canns/analyzer/data/__init__.py +5 -1
- canns/analyzer/data/asa/__init__.py +27 -12
- canns/analyzer/data/asa/cohospace.py +336 -10
- canns/analyzer/data/asa/config.py +3 -0
- canns/analyzer/data/asa/embedding.py +48 -45
- canns/analyzer/data/asa/path.py +104 -2
- canns/analyzer/data/asa/plotting.py +88 -19
- canns/analyzer/data/asa/tda.py +11 -4
- canns/analyzer/data/cell_classification/__init__.py +97 -0
- canns/analyzer/data/cell_classification/core/__init__.py +26 -0
- canns/analyzer/data/cell_classification/core/grid_cells.py +633 -0
- canns/analyzer/data/cell_classification/core/grid_modules_leiden.py +288 -0
- canns/analyzer/data/cell_classification/core/head_direction.py +347 -0
- canns/analyzer/data/cell_classification/core/spatial_analysis.py +431 -0
- canns/analyzer/data/cell_classification/io/__init__.py +5 -0
- canns/analyzer/data/cell_classification/io/matlab_loader.py +417 -0
- canns/analyzer/data/cell_classification/utils/__init__.py +39 -0
- canns/analyzer/data/cell_classification/utils/circular_stats.py +383 -0
- canns/analyzer/data/cell_classification/utils/correlation.py +318 -0
- canns/analyzer/data/cell_classification/utils/geometry.py +442 -0
- canns/analyzer/data/cell_classification/utils/image_processing.py +416 -0
- canns/analyzer/data/cell_classification/visualization/__init__.py +19 -0
- canns/analyzer/data/cell_classification/visualization/grid_plots.py +292 -0
- canns/analyzer/data/cell_classification/visualization/hd_plots.py +200 -0
- canns/analyzer/metrics/__init__.py +2 -1
- canns/analyzer/visualization/core/config.py +46 -4
- canns/data/__init__.py +6 -1
- canns/data/datasets.py +154 -1
- canns/data/loaders.py +37 -0
- canns/pipeline/__init__.py +13 -9
- canns/pipeline/__main__.py +6 -0
- canns/pipeline/asa/runner.py +105 -41
- canns/pipeline/asa_gui/__init__.py +68 -0
- canns/pipeline/asa_gui/__main__.py +6 -0
- canns/pipeline/asa_gui/analysis_modes/__init__.py +42 -0
- canns/pipeline/asa_gui/analysis_modes/base.py +39 -0
- canns/pipeline/asa_gui/analysis_modes/batch_mode.py +21 -0
- canns/pipeline/asa_gui/analysis_modes/cohomap_mode.py +56 -0
- canns/pipeline/asa_gui/analysis_modes/cohospace_mode.py +194 -0
- canns/pipeline/asa_gui/analysis_modes/decode_mode.py +52 -0
- canns/pipeline/asa_gui/analysis_modes/fr_mode.py +81 -0
- canns/pipeline/asa_gui/analysis_modes/frm_mode.py +92 -0
- canns/pipeline/asa_gui/analysis_modes/gridscore_mode.py +123 -0
- canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py +199 -0
- canns/pipeline/asa_gui/analysis_modes/tda_mode.py +112 -0
- canns/pipeline/asa_gui/app.py +29 -0
- canns/pipeline/asa_gui/controllers/__init__.py +6 -0
- canns/pipeline/asa_gui/controllers/analysis_controller.py +59 -0
- canns/pipeline/asa_gui/controllers/preprocess_controller.py +89 -0
- canns/pipeline/asa_gui/core/__init__.py +15 -0
- canns/pipeline/asa_gui/core/cache.py +14 -0
- canns/pipeline/asa_gui/core/runner.py +1936 -0
- canns/pipeline/asa_gui/core/state.py +324 -0
- canns/pipeline/asa_gui/core/worker.py +260 -0
- canns/pipeline/asa_gui/main_window.py +184 -0
- canns/pipeline/asa_gui/models/__init__.py +7 -0
- canns/pipeline/asa_gui/models/config.py +14 -0
- canns/pipeline/asa_gui/models/job.py +31 -0
- canns/pipeline/asa_gui/models/presets.py +21 -0
- canns/pipeline/asa_gui/resources/__init__.py +16 -0
- canns/pipeline/asa_gui/resources/dark.qss +167 -0
- canns/pipeline/asa_gui/resources/light.qss +163 -0
- canns/pipeline/asa_gui/resources/styles.qss +130 -0
- canns/pipeline/asa_gui/utils/__init__.py +1 -0
- canns/pipeline/asa_gui/utils/formatters.py +15 -0
- canns/pipeline/asa_gui/utils/io_adapters.py +40 -0
- canns/pipeline/asa_gui/utils/validators.py +41 -0
- canns/pipeline/asa_gui/views/__init__.py +1 -0
- canns/pipeline/asa_gui/views/help_content.py +171 -0
- canns/pipeline/asa_gui/views/pages/__init__.py +6 -0
- canns/pipeline/asa_gui/views/pages/analysis_page.py +565 -0
- canns/pipeline/asa_gui/views/pages/preprocess_page.py +492 -0
- canns/pipeline/asa_gui/views/panels/__init__.py +1 -0
- canns/pipeline/asa_gui/views/widgets/__init__.py +21 -0
- canns/pipeline/asa_gui/views/widgets/artifacts_tab.py +44 -0
- canns/pipeline/asa_gui/views/widgets/drop_zone.py +80 -0
- canns/pipeline/asa_gui/views/widgets/file_list.py +27 -0
- canns/pipeline/asa_gui/views/widgets/gridscore_tab.py +308 -0
- canns/pipeline/asa_gui/views/widgets/help_dialog.py +27 -0
- canns/pipeline/asa_gui/views/widgets/image_tab.py +50 -0
- canns/pipeline/asa_gui/views/widgets/image_viewer.py +97 -0
- canns/pipeline/asa_gui/views/widgets/log_box.py +16 -0
- canns/pipeline/asa_gui/views/widgets/pathcompare_tab.py +200 -0
- canns/pipeline/asa_gui/views/widgets/popup_combo.py +25 -0
- canns/pipeline/gallery/__init__.py +15 -5
- canns/pipeline/gallery/__main__.py +11 -0
- canns/pipeline/gallery/app.py +705 -0
- canns/pipeline/gallery/runner.py +790 -0
- canns/pipeline/gallery/state.py +51 -0
- canns/pipeline/gallery/styles.tcss +123 -0
- canns/pipeline/launcher.py +81 -0
- {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/METADATA +11 -1
- canns-0.14.0.dist-info/RECORD +163 -0
- canns-0.14.0.dist-info/entry_points.txt +5 -0
- canns/pipeline/_base.py +0 -50
- canns-0.13.1.dist-info/RECORD +0 -89
- canns-0.13.1.dist-info/entry_points.txt +0 -3
- {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/WHEEL +0 -0
- {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)))
|