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,199 @@
|
|
|
1
|
+
"""PathCompare analysis mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtWidgets import (
|
|
6
|
+
QCheckBox,
|
|
7
|
+
QDoubleSpinBox,
|
|
8
|
+
QFormLayout,
|
|
9
|
+
QGroupBox,
|
|
10
|
+
QHBoxLayout,
|
|
11
|
+
QLabel,
|
|
12
|
+
QLineEdit,
|
|
13
|
+
QSpinBox,
|
|
14
|
+
QWidget,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from ..views.widgets.popup_combo import PopupComboBox
|
|
18
|
+
from .base import AbstractAnalysisMode, configure_form_layout
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PathCompareMode(AbstractAnalysisMode):
|
|
22
|
+
name = "pathcompare"
|
|
23
|
+
display_name = "Path Compare (CohoMap required)"
|
|
24
|
+
|
|
25
|
+
def create_params_widget(self) -> QGroupBox:
|
|
26
|
+
box = QGroupBox("PathCompare Parameters")
|
|
27
|
+
form = QFormLayout(box)
|
|
28
|
+
configure_form_layout(form)
|
|
29
|
+
|
|
30
|
+
self.angle_scale = PopupComboBox()
|
|
31
|
+
self.angle_scale.addItems(["auto", "rad", "deg", "unit"])
|
|
32
|
+
self.angle_scale.setCurrentText("auto")
|
|
33
|
+
|
|
34
|
+
self.dim_mode = PopupComboBox()
|
|
35
|
+
self.dim_mode.addItem("2D", userData="2d")
|
|
36
|
+
self.dim_mode.addItem("1D", userData="1d")
|
|
37
|
+
self.dim_mode.setCurrentIndex(0)
|
|
38
|
+
|
|
39
|
+
self.dim = QSpinBox()
|
|
40
|
+
self.dim.setRange(1, 10)
|
|
41
|
+
self.dim.setValue(1)
|
|
42
|
+
|
|
43
|
+
self.dim1 = QSpinBox()
|
|
44
|
+
self.dim1.setRange(1, 10)
|
|
45
|
+
self.dim1.setValue(1)
|
|
46
|
+
|
|
47
|
+
self.dim2 = QSpinBox()
|
|
48
|
+
self.dim2.setRange(1, 10)
|
|
49
|
+
self.dim2.setValue(2)
|
|
50
|
+
|
|
51
|
+
self.use_box = QCheckBox("Use coordsbox / times_box")
|
|
52
|
+
self.use_box.setChecked(False)
|
|
53
|
+
|
|
54
|
+
self.interp_full = QCheckBox("Interpolate to full trajectory")
|
|
55
|
+
self.interp_full.setChecked(True)
|
|
56
|
+
self.interp_full.setEnabled(False)
|
|
57
|
+
|
|
58
|
+
self.coords_key = QLineEdit()
|
|
59
|
+
self.coords_key.setPlaceholderText("coords / coordsbox (optional)")
|
|
60
|
+
self.times_key = QLineEdit()
|
|
61
|
+
self.times_key.setPlaceholderText("times_box (optional)")
|
|
62
|
+
|
|
63
|
+
self.slice_mode = PopupComboBox()
|
|
64
|
+
self.slice_mode.addItem("Time (tmin/tmax)", userData="time")
|
|
65
|
+
self.slice_mode.addItem("Index (imin/imax)", userData="index")
|
|
66
|
+
self.slice_mode.setCurrentIndex(0)
|
|
67
|
+
|
|
68
|
+
self.tmin = QDoubleSpinBox()
|
|
69
|
+
self.tmin.setRange(-1e9, 1e9)
|
|
70
|
+
self.tmin.setDecimals(4)
|
|
71
|
+
self.tmin.setValue(-1.0)
|
|
72
|
+
|
|
73
|
+
self.tmax = QDoubleSpinBox()
|
|
74
|
+
self.tmax.setRange(-1e9, 1e9)
|
|
75
|
+
self.tmax.setDecimals(4)
|
|
76
|
+
self.tmax.setValue(-1.0)
|
|
77
|
+
|
|
78
|
+
self.imin = QSpinBox()
|
|
79
|
+
self.imin.setRange(-1, 1_000_000_000)
|
|
80
|
+
self.imin.setValue(-1)
|
|
81
|
+
|
|
82
|
+
self.imax = QSpinBox()
|
|
83
|
+
self.imax.setRange(-1, 1_000_000_000)
|
|
84
|
+
self.imax.setValue(-1)
|
|
85
|
+
|
|
86
|
+
self.stride = QSpinBox()
|
|
87
|
+
self.stride.setRange(1, 100000)
|
|
88
|
+
self.stride.setValue(1)
|
|
89
|
+
|
|
90
|
+
self.tail = QSpinBox()
|
|
91
|
+
self.tail.setRange(0, 100000)
|
|
92
|
+
self.tail.setValue(200)
|
|
93
|
+
|
|
94
|
+
self.fps = QSpinBox()
|
|
95
|
+
self.fps.setRange(1, 240)
|
|
96
|
+
self.fps.setValue(30)
|
|
97
|
+
|
|
98
|
+
self.no_wrap = QCheckBox("Disable angle wrap")
|
|
99
|
+
self.no_wrap.setChecked(False)
|
|
100
|
+
|
|
101
|
+
self.save_gif = QCheckBox("Save GIF")
|
|
102
|
+
self.save_gif.setChecked(False)
|
|
103
|
+
|
|
104
|
+
form.addRow("Dim mode", self.dim_mode)
|
|
105
|
+
|
|
106
|
+
self._dims1d_label = QLabel("Dim")
|
|
107
|
+
dims_1d = QWidget()
|
|
108
|
+
dims_1d_layout = QHBoxLayout(dims_1d)
|
|
109
|
+
dims_1d_layout.setContentsMargins(0, 0, 0, 0)
|
|
110
|
+
dims_1d_layout.addWidget(self.dim)
|
|
111
|
+
dims_1d_layout.addStretch(1)
|
|
112
|
+
|
|
113
|
+
self._dims2d_label = QLabel("Dim X / Dim Y")
|
|
114
|
+
dims_2d = QWidget()
|
|
115
|
+
dims_2d_layout = QHBoxLayout(dims_2d)
|
|
116
|
+
dims_2d_layout.setContentsMargins(0, 0, 0, 0)
|
|
117
|
+
dims_2d_layout.addWidget(QLabel("dim1"))
|
|
118
|
+
dims_2d_layout.addWidget(self.dim1)
|
|
119
|
+
dims_2d_layout.addSpacing(8)
|
|
120
|
+
dims_2d_layout.addWidget(QLabel("dim2"))
|
|
121
|
+
dims_2d_layout.addWidget(self.dim2)
|
|
122
|
+
dims_2d_layout.addStretch(1)
|
|
123
|
+
|
|
124
|
+
form.addRow(self._dims1d_label, dims_1d)
|
|
125
|
+
form.addRow(self._dims2d_label, dims_2d)
|
|
126
|
+
form.addRow(self.use_box)
|
|
127
|
+
form.addRow(self.interp_full)
|
|
128
|
+
form.addRow("coords key", self.coords_key)
|
|
129
|
+
form.addRow("times key", self.times_key)
|
|
130
|
+
form.addRow("Slice mode", self.slice_mode)
|
|
131
|
+
form.addRow("tmin (sec, -1=auto)", self.tmin)
|
|
132
|
+
form.addRow("tmax (sec, -1=auto)", self.tmax)
|
|
133
|
+
form.addRow("imin (-1=auto)", self.imin)
|
|
134
|
+
form.addRow("imax (-1=auto)", self.imax)
|
|
135
|
+
form.addRow("stride", self.stride)
|
|
136
|
+
form.addRow("tail (frames)", self.tail)
|
|
137
|
+
form.addRow("fps", self.fps)
|
|
138
|
+
form.addRow("theta scale", self.angle_scale)
|
|
139
|
+
form.addRow(self.no_wrap)
|
|
140
|
+
form.addRow(self.save_gif)
|
|
141
|
+
|
|
142
|
+
def _refresh_dim_mode() -> None:
|
|
143
|
+
mode = str(self.dim_mode.currentData() or "2d")
|
|
144
|
+
is_1d = mode == "1d"
|
|
145
|
+
self._dims1d_label.setVisible(is_1d)
|
|
146
|
+
dims_1d.setVisible(is_1d)
|
|
147
|
+
self._dims2d_label.setVisible(not is_1d)
|
|
148
|
+
dims_2d.setVisible(not is_1d)
|
|
149
|
+
|
|
150
|
+
def _refresh_enabled() -> None:
|
|
151
|
+
use_box = bool(self.use_box.isChecked())
|
|
152
|
+
self.interp_full.setEnabled(use_box)
|
|
153
|
+
|
|
154
|
+
def _refresh_slice_mode() -> None:
|
|
155
|
+
is_time = self.slice_mode.currentData() == "time"
|
|
156
|
+
self.tmin.setEnabled(is_time)
|
|
157
|
+
self.tmax.setEnabled(is_time)
|
|
158
|
+
self.imin.setEnabled(not is_time)
|
|
159
|
+
self.imax.setEnabled(not is_time)
|
|
160
|
+
|
|
161
|
+
self.dim_mode.currentIndexChanged.connect(_refresh_dim_mode)
|
|
162
|
+
self.use_box.toggled.connect(_refresh_enabled)
|
|
163
|
+
self.slice_mode.currentIndexChanged.connect(_refresh_slice_mode)
|
|
164
|
+
_refresh_dim_mode()
|
|
165
|
+
_refresh_enabled()
|
|
166
|
+
_refresh_slice_mode()
|
|
167
|
+
|
|
168
|
+
return box
|
|
169
|
+
|
|
170
|
+
def collect_params(self) -> dict:
|
|
171
|
+
tmin = float(self.tmin.value())
|
|
172
|
+
tmax = float(self.tmax.value())
|
|
173
|
+
imin = int(self.imin.value())
|
|
174
|
+
imax = int(self.imax.value())
|
|
175
|
+
tmin = None if tmin < 0 else tmin
|
|
176
|
+
tmax = None if tmax < 0 else tmax
|
|
177
|
+
imin = None if imin < 0 else imin
|
|
178
|
+
imax = None if imax < 0 else imax
|
|
179
|
+
return {
|
|
180
|
+
"angle_scale": str(self.angle_scale.currentText()),
|
|
181
|
+
"dim_mode": str(self.dim_mode.currentData() or "2d"),
|
|
182
|
+
"dim": int(self.dim.value()),
|
|
183
|
+
"dim1": int(self.dim1.value()),
|
|
184
|
+
"dim2": int(self.dim2.value()),
|
|
185
|
+
"use_box": bool(self.use_box.isChecked()),
|
|
186
|
+
"interp_full": bool(self.interp_full.isChecked()),
|
|
187
|
+
"coords_key": self.coords_key.text().strip() or None,
|
|
188
|
+
"times_key": self.times_key.text().strip() or None,
|
|
189
|
+
"slice_mode": str(self.slice_mode.currentData() or "time"),
|
|
190
|
+
"tmin": tmin,
|
|
191
|
+
"tmax": tmax,
|
|
192
|
+
"imin": imin,
|
|
193
|
+
"imax": imax,
|
|
194
|
+
"stride": int(self.stride.value()),
|
|
195
|
+
"tail": int(self.tail.value()),
|
|
196
|
+
"fps": int(self.fps.value()),
|
|
197
|
+
"no_wrap": bool(self.no_wrap.isChecked()),
|
|
198
|
+
"animation_format": "gif" if self.save_gif.isChecked() else "none",
|
|
199
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""TDA analysis mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtWidgets import QCheckBox, QFormLayout, QGroupBox, QSpinBox
|
|
6
|
+
|
|
7
|
+
from ..views.widgets.popup_combo import PopupComboBox
|
|
8
|
+
from .base import AbstractAnalysisMode, configure_form_layout
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TDAMode(AbstractAnalysisMode):
|
|
12
|
+
name = "tda"
|
|
13
|
+
display_name = "TDA"
|
|
14
|
+
|
|
15
|
+
def create_params_widget(self) -> QGroupBox:
|
|
16
|
+
box = QGroupBox("TDA parameters")
|
|
17
|
+
form = QFormLayout(box)
|
|
18
|
+
configure_form_layout(form)
|
|
19
|
+
|
|
20
|
+
self.dim = QSpinBox()
|
|
21
|
+
self.dim.setRange(1, 50)
|
|
22
|
+
self.dim.setValue(6)
|
|
23
|
+
self.dim.setToolTip("PCA 维度(常见起点 6–12)。")
|
|
24
|
+
|
|
25
|
+
self.num_times = QSpinBox()
|
|
26
|
+
self.num_times.setRange(1, 50)
|
|
27
|
+
self.num_times.setValue(5)
|
|
28
|
+
self.num_times.setToolTip("时间下采样步长;越大越快但可能丢细节。")
|
|
29
|
+
|
|
30
|
+
self.active_times = QSpinBox()
|
|
31
|
+
self.active_times.setRange(1, 10_000_000)
|
|
32
|
+
self.active_times.setValue(15000)
|
|
33
|
+
self.active_times.setToolTip("选取最活跃时间点数;过小不稳,过大更慢。")
|
|
34
|
+
|
|
35
|
+
self.k = QSpinBox()
|
|
36
|
+
self.k.setRange(1, 200_000)
|
|
37
|
+
self.k.setValue(1000)
|
|
38
|
+
self.k.setToolTip("采样/去噪相关参数,影响速度与稳定性。")
|
|
39
|
+
|
|
40
|
+
self.n_points = QSpinBox()
|
|
41
|
+
self.n_points.setRange(10, 500_000)
|
|
42
|
+
self.n_points.setValue(1200)
|
|
43
|
+
self.n_points.setToolTip("点云代表点数量,越大越慢。")
|
|
44
|
+
|
|
45
|
+
self.metric = PopupComboBox()
|
|
46
|
+
self.metric.addItems(["cosine", "euclidean", "manhattan"])
|
|
47
|
+
self.metric.setToolTip("距离度量;推荐 cosine。")
|
|
48
|
+
|
|
49
|
+
self.nbs = QSpinBox()
|
|
50
|
+
self.nbs.setRange(1, 200_000)
|
|
51
|
+
self.nbs.setValue(800)
|
|
52
|
+
self.nbs.setToolTip("邻域规模参数,影响稳定性与速度。")
|
|
53
|
+
|
|
54
|
+
self.maxdim = QSpinBox()
|
|
55
|
+
self.maxdim.setRange(0, 3)
|
|
56
|
+
self.maxdim.setValue(2)
|
|
57
|
+
self.maxdim.setToolTip("最大同调维度;先 1 再 2。")
|
|
58
|
+
|
|
59
|
+
self.coeff = QSpinBox()
|
|
60
|
+
self.coeff.setRange(2, 997)
|
|
61
|
+
self.coeff.setValue(47)
|
|
62
|
+
self.coeff.setToolTip("有限域系数(默认 47)。")
|
|
63
|
+
|
|
64
|
+
self.standardize = QCheckBox()
|
|
65
|
+
self.standardize.setChecked(True)
|
|
66
|
+
|
|
67
|
+
self.do_shuffle = QCheckBox()
|
|
68
|
+
self.do_shuffle.setChecked(False)
|
|
69
|
+
self.do_shuffle.setToolTip("显著性检验;代价高,建议少量。")
|
|
70
|
+
|
|
71
|
+
self.num_shuffles = QSpinBox()
|
|
72
|
+
self.num_shuffles.setRange(0, 5000)
|
|
73
|
+
self.num_shuffles.setValue(100)
|
|
74
|
+
self.num_shuffles.setEnabled(False)
|
|
75
|
+
self.do_shuffle.toggled.connect(self.num_shuffles.setEnabled)
|
|
76
|
+
self.num_shuffles.setToolTip("Shuffle 次数(越多越慢)。")
|
|
77
|
+
|
|
78
|
+
form.addRow("TDA dim", self.dim)
|
|
79
|
+
form.addRow("num_times", self.num_times)
|
|
80
|
+
form.addRow("active_times", self.active_times)
|
|
81
|
+
form.addRow("k (sampling)", self.k)
|
|
82
|
+
form.addRow("n_points", self.n_points)
|
|
83
|
+
form.addRow("metric", self.metric)
|
|
84
|
+
form.addRow("nbs", self.nbs)
|
|
85
|
+
form.addRow("maxdim", self.maxdim)
|
|
86
|
+
form.addRow("coeff", self.coeff)
|
|
87
|
+
form.addRow("Shuffle: Enable", self.do_shuffle)
|
|
88
|
+
form.addRow("num_shuffles", self.num_shuffles)
|
|
89
|
+
|
|
90
|
+
return box
|
|
91
|
+
|
|
92
|
+
def collect_params(self) -> dict:
|
|
93
|
+
return {
|
|
94
|
+
"dim": int(self.dim.value()),
|
|
95
|
+
"num_times": int(self.num_times.value()),
|
|
96
|
+
"active_times": int(self.active_times.value()),
|
|
97
|
+
"k": int(self.k.value()),
|
|
98
|
+
"n_points": int(self.n_points.value()),
|
|
99
|
+
"metric": str(self.metric.currentText()),
|
|
100
|
+
"nbs": int(self.nbs.value()),
|
|
101
|
+
"maxdim": int(self.maxdim.value()),
|
|
102
|
+
"coeff": int(self.coeff.value()),
|
|
103
|
+
"standardize": bool(self.standardize.isChecked()),
|
|
104
|
+
"do_shuffle": bool(self.do_shuffle.isChecked()),
|
|
105
|
+
"num_shuffles": int(self.num_shuffles.value()),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
def apply_preset(self, preset: str) -> None:
|
|
109
|
+
if preset == "grid":
|
|
110
|
+
self.maxdim.setValue(2)
|
|
111
|
+
elif preset == "hd":
|
|
112
|
+
self.maxdim.setValue(1)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Application bootstrap for ASA GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QSettings
|
|
6
|
+
from PySide6.QtWidgets import QApplication
|
|
7
|
+
|
|
8
|
+
from .main_window import MainWindow
|
|
9
|
+
from .resources import load_theme_qss
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ASAGuiApp(MainWindow):
|
|
13
|
+
def __init__(self, parent=None) -> None:
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
self._apply_styles()
|
|
16
|
+
|
|
17
|
+
def _apply_styles(self) -> None:
|
|
18
|
+
try:
|
|
19
|
+
settings = QSettings("canns", "asa_gui")
|
|
20
|
+
theme = settings.value("theme", "Light")
|
|
21
|
+
qss = load_theme_qss(str(theme))
|
|
22
|
+
app = QApplication.instance()
|
|
23
|
+
app.setStyleSheet(qss)
|
|
24
|
+
font = app.font()
|
|
25
|
+
if font.pointSize() <= 0:
|
|
26
|
+
font.setPointSize(10)
|
|
27
|
+
app.setFont(font)
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Controller for analysis workflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QObject
|
|
8
|
+
|
|
9
|
+
from ..core import PipelineRunner, StateManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AnalysisController(QObject):
|
|
13
|
+
def __init__(self, state_manager: StateManager, runner: PipelineRunner, parent=None) -> None:
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
self._state_manager = state_manager
|
|
16
|
+
self._runner = runner
|
|
17
|
+
|
|
18
|
+
def update_analysis(self, *, analysis_mode: str, analysis_params: dict) -> None:
|
|
19
|
+
self._state_manager.batch_update(
|
|
20
|
+
analysis_mode=analysis_mode,
|
|
21
|
+
analysis_params=analysis_params,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def get_state(self):
|
|
25
|
+
return self._state_manager.state
|
|
26
|
+
|
|
27
|
+
def run_analysis(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
worker_manager,
|
|
31
|
+
on_log: Callable[[str], None],
|
|
32
|
+
on_progress: Callable[[int], None],
|
|
33
|
+
on_finished: Callable[[object], None],
|
|
34
|
+
on_error: Callable[[str], None],
|
|
35
|
+
on_cleanup: Callable[[], None] | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
state = self._state_manager.state
|
|
38
|
+
self._state_manager.update(is_running=True, current_stage="analysis")
|
|
39
|
+
|
|
40
|
+
worker_manager.start(
|
|
41
|
+
self._runner.run_analysis,
|
|
42
|
+
state,
|
|
43
|
+
on_log=on_log,
|
|
44
|
+
on_progress=on_progress,
|
|
45
|
+
on_finished=on_finished,
|
|
46
|
+
on_error=on_error,
|
|
47
|
+
on_cleanup=on_cleanup,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def finalize_analysis(self, artifacts: dict) -> None:
|
|
51
|
+
self._state_manager.update(
|
|
52
|
+
artifacts=artifacts,
|
|
53
|
+
is_running=False,
|
|
54
|
+
current_stage="",
|
|
55
|
+
progress=0,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def mark_idle(self) -> None:
|
|
59
|
+
self._state_manager.update(is_running=False, current_stage="", progress=0)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Controller for preprocessing workflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QObject
|
|
8
|
+
|
|
9
|
+
from ..core import PipelineRunner, StateManager
|
|
10
|
+
from ..core.state import relative_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PreprocessController(QObject):
|
|
14
|
+
def __init__(self, state_manager: StateManager, runner: PipelineRunner, parent=None) -> None:
|
|
15
|
+
super().__init__(parent)
|
|
16
|
+
self._state_manager = state_manager
|
|
17
|
+
self._runner = runner
|
|
18
|
+
|
|
19
|
+
def update_inputs(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
input_mode: str,
|
|
23
|
+
preset: str,
|
|
24
|
+
asa_file: str | None,
|
|
25
|
+
neuron_file: str | None,
|
|
26
|
+
traj_file: str | None,
|
|
27
|
+
preprocess_method: str,
|
|
28
|
+
preprocess_params: dict,
|
|
29
|
+
preclass: str,
|
|
30
|
+
preclass_params: dict,
|
|
31
|
+
) -> None:
|
|
32
|
+
state = self._state_manager.state
|
|
33
|
+
asa_path = self._to_path(asa_file)
|
|
34
|
+
neuron_path = self._to_path(neuron_file)
|
|
35
|
+
traj_path = self._to_path(traj_file)
|
|
36
|
+
self._state_manager.batch_update(
|
|
37
|
+
input_mode=input_mode,
|
|
38
|
+
preset=preset,
|
|
39
|
+
asa_file=relative_path(state, path=asa_path) if asa_path is not None else None,
|
|
40
|
+
neuron_file=relative_path(state, path=neuron_path) if neuron_path is not None else None,
|
|
41
|
+
traj_file=relative_path(state, path=traj_path) if traj_path is not None else None,
|
|
42
|
+
preprocess_method=preprocess_method,
|
|
43
|
+
preprocess_params=preprocess_params,
|
|
44
|
+
preclass=preclass,
|
|
45
|
+
preclass_params=preclass_params,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def run_preprocess(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
worker_manager,
|
|
52
|
+
on_log: Callable[[str], None],
|
|
53
|
+
on_progress: Callable[[int], None],
|
|
54
|
+
on_finished: Callable[[object], None],
|
|
55
|
+
on_error: Callable[[str], None],
|
|
56
|
+
on_cleanup: Callable[[], None] | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
state = self._state_manager.state
|
|
59
|
+
self._state_manager.update(is_running=True, current_stage="preprocess")
|
|
60
|
+
|
|
61
|
+
worker_manager.start(
|
|
62
|
+
self._runner.run_preprocessing,
|
|
63
|
+
state,
|
|
64
|
+
on_log=on_log,
|
|
65
|
+
on_progress=on_progress,
|
|
66
|
+
on_finished=on_finished,
|
|
67
|
+
on_error=on_error,
|
|
68
|
+
on_cleanup=on_cleanup,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def finalize_preprocess(self) -> None:
|
|
72
|
+
self._state_manager.update(
|
|
73
|
+
embed_data=self._runner.embed_data,
|
|
74
|
+
aligned_pos=self._runner.aligned_pos,
|
|
75
|
+
is_running=False,
|
|
76
|
+
current_stage="",
|
|
77
|
+
progress=0,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def mark_idle(self) -> None:
|
|
81
|
+
self._state_manager.update(is_running=False, current_stage="", progress=0)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _to_path(path: str | None):
|
|
85
|
+
if path is None:
|
|
86
|
+
return None
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
|
|
89
|
+
return Path(path)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Core infrastructure for ASA GUI."""
|
|
2
|
+
|
|
3
|
+
from .runner import PipelineResult, PipelineRunner, ProcessingError
|
|
4
|
+
from .state import StateManager, WorkflowState
|
|
5
|
+
from .worker import AnalysisWorker, WorkerManager
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"PipelineResult",
|
|
9
|
+
"PipelineRunner",
|
|
10
|
+
"ProcessingError",
|
|
11
|
+
"StateManager",
|
|
12
|
+
"WorkflowState",
|
|
13
|
+
"AnalysisWorker",
|
|
14
|
+
"WorkerManager",
|
|
15
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Cache helpers for ASA GUI pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def md5_file(path: Path) -> str:
|
|
10
|
+
md5 = hashlib.md5()
|
|
11
|
+
with path.open("rb") as f:
|
|
12
|
+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
13
|
+
md5.update(chunk)
|
|
14
|
+
return md5.hexdigest()
|