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,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,6 @@
1
+ """Controllers for ASA GUI."""
2
+
3
+ from .analysis_controller import AnalysisController
4
+ from .preprocess_controller import PreprocessController
5
+
6
+ __all__ = ["AnalysisController", "PreprocessController"]
@@ -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()