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,56 @@
1
+ """CohoMap 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 CohoMapMode(AbstractAnalysisMode):
12
+ name = "cohomap"
13
+ display_name = "CohoMap (TDA + decode)"
14
+
15
+ def create_params_widget(self) -> QGroupBox:
16
+ box = QGroupBox("CohoMap Parameters")
17
+ form = QFormLayout(box)
18
+ configure_form_layout(form)
19
+
20
+ self.decode_version = PopupComboBox()
21
+ self.decode_version.addItems(["v2", "v0"])
22
+
23
+ self.num_circ = QSpinBox()
24
+ self.num_circ.setRange(1, 50)
25
+ self.num_circ.setValue(2)
26
+
27
+ self.real_ground = QCheckBox()
28
+ self.real_ground.setChecked(True)
29
+
30
+ self.real_of = QCheckBox()
31
+ self.real_of.setChecked(True)
32
+
33
+ self.subsample = QSpinBox()
34
+ self.subsample.setRange(1, 5000)
35
+ self.subsample.setValue(10)
36
+
37
+ form.addRow("Decode version", self.decode_version)
38
+ form.addRow("Decode num_circ", self.num_circ)
39
+ form.addRow("CohoMap subsample", self.subsample)
40
+
41
+ return box
42
+
43
+ def collect_params(self) -> dict:
44
+ return {
45
+ "decode_version": str(self.decode_version.currentText()),
46
+ "num_circ": int(self.num_circ.value()),
47
+ "real_ground": bool(self.real_ground.isChecked()),
48
+ "real_of": bool(self.real_of.isChecked()),
49
+ "cohomap_subsample": int(self.subsample.value()),
50
+ }
51
+
52
+ def apply_preset(self, preset: str) -> None:
53
+ if preset == "grid":
54
+ self.num_circ.setValue(2)
55
+ elif preset == "hd":
56
+ self.num_circ.setValue(1)
@@ -0,0 +1,194 @@
1
+ """CohoSpace 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
+ QPushButton,
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 CohoSpaceMode(AbstractAnalysisMode):
22
+ name = "cohospace"
23
+ display_name = "CohoSpace / CohoScore"
24
+
25
+ def create_params_widget(self) -> QGroupBox:
26
+ box = QGroupBox("CohoSpace / CohoScore")
27
+ form = QFormLayout(box)
28
+ configure_form_layout(form)
29
+
30
+ self.dim_mode = PopupComboBox()
31
+ self.dim_mode.addItem("2D", userData="2d")
32
+ self.dim_mode.addItem("1D", userData="1d")
33
+ self.dim_mode.setCurrentIndex(0)
34
+
35
+ self.dim = QSpinBox()
36
+ self.dim.setRange(1, 10)
37
+ self.dim.setValue(1)
38
+
39
+ self.dim1 = QSpinBox()
40
+ self.dim1.setRange(1, 10)
41
+ self.dim1.setValue(1)
42
+
43
+ self.dim2 = QSpinBox()
44
+ self.dim2.setRange(1, 10)
45
+ self.dim2.setValue(2)
46
+
47
+ self.mode = PopupComboBox()
48
+ self.mode.addItems(["spike", "fr"])
49
+ self.mode.setCurrentText("spike")
50
+
51
+ self.top_percent = QDoubleSpinBox()
52
+ self.top_percent.setRange(0.1, 50.0)
53
+ self.top_percent.setSingleStep(0.5)
54
+ self.top_percent.setValue(2.0)
55
+
56
+ self.view = PopupComboBox()
57
+ self.view.addItem("single neuron", userData="single")
58
+ self.view.addItem("all neurons (aggregate)", userData="population")
59
+ self.view.setCurrentIndex(0)
60
+
61
+ self.subsample = QSpinBox()
62
+ self.subsample.setRange(1, 100)
63
+ self.subsample.setValue(2)
64
+
65
+ self.unfold = PopupComboBox()
66
+ self.unfold.addItems(["square", "skew"])
67
+
68
+ self.skew_show_grid = QCheckBox("Skew: show grid")
69
+ self.skew_show_grid.setChecked(True)
70
+
71
+ self.skew_tiles = QSpinBox()
72
+ self.skew_tiles.setRange(0, 10)
73
+ self.skew_tiles.setValue(0)
74
+
75
+ self.top_k = QSpinBox()
76
+ self.top_k.setRange(1, 10_000_000)
77
+ self.top_k.setValue(10)
78
+
79
+ self.neuron_id = QSpinBox()
80
+ self.neuron_id.setRange(0, 1_000_000)
81
+ self.neuron_id.setValue(0)
82
+
83
+ self.btn_prev = QPushButton("←")
84
+ self.btn_next = QPushButton("→")
85
+ self.btn_show = QPushButton("Show")
86
+ self._neuron_row = QWidget()
87
+ neuron_row_layout = QHBoxLayout(self._neuron_row)
88
+ neuron_row_layout.setContentsMargins(0, 0, 0, 0)
89
+ neuron_row_layout.addWidget(self.btn_prev)
90
+ neuron_row_layout.addWidget(self.neuron_id, 1)
91
+ neuron_row_layout.addWidget(self.btn_next)
92
+ neuron_row_layout.addSpacing(8)
93
+ neuron_row_layout.addWidget(self.btn_show)
94
+
95
+ self.enable_score = QCheckBox("Compute CohoScore & top-K")
96
+ self.enable_score.setChecked(True)
97
+
98
+ self.use_best = QCheckBox("Use best neuron by CohoScore")
99
+ self.use_best.setChecked(True)
100
+
101
+ form.addRow("Dim mode", self.dim_mode)
102
+
103
+ self._dims1d_label = QLabel("Dim")
104
+ self._dims1d_wrap = QWidget()
105
+ dims_1d_layout = QHBoxLayout(self._dims1d_wrap)
106
+ dims_1d_layout.setContentsMargins(0, 0, 0, 0)
107
+ dims_1d_layout.addWidget(self.dim)
108
+ dims_1d_layout.addStretch(1)
109
+
110
+ self._dims2d_label = QLabel("Dim X / Dim Y")
111
+ self._dims2d_wrap = QWidget()
112
+ dims_2d_layout = QHBoxLayout(self._dims2d_wrap)
113
+ dims_2d_layout.setContentsMargins(0, 0, 0, 0)
114
+ dims_2d_layout.addWidget(QLabel("dim1"))
115
+ dims_2d_layout.addWidget(self.dim1)
116
+ dims_2d_layout.addSpacing(8)
117
+ dims_2d_layout.addWidget(QLabel("dim2"))
118
+ dims_2d_layout.addWidget(self.dim2)
119
+ dims_2d_layout.addStretch(1)
120
+
121
+ form.addRow(self._dims1d_label, self._dims1d_wrap)
122
+ form.addRow(self._dims2d_label, self._dims2d_wrap)
123
+ form.addRow("Unfold", self.unfold)
124
+ form.addRow("", self.skew_show_grid)
125
+ form.addRow("Skew tiles", self.skew_tiles)
126
+ form.addRow("Mode (spike / fr)", self.mode)
127
+ form.addRow("Top % (threshold)", self.top_percent)
128
+ form.addRow(self.enable_score)
129
+ form.addRow("View", self.view)
130
+ form.addRow("Top-K neurons", self.top_k)
131
+ form.addRow("Neuron id", self._neuron_row)
132
+ form.addRow("", self.use_best)
133
+
134
+ self.dim_mode.currentIndexChanged.connect(self._refresh_dim_mode)
135
+ self.view.currentIndexChanged.connect(self._refresh_view)
136
+ self.enable_score.toggled.connect(self._refresh_view)
137
+ self.btn_prev.clicked.connect(lambda: self._shift(-1))
138
+ self.btn_next.clicked.connect(lambda: self._shift(+1))
139
+ self._refresh_dim_mode()
140
+ self._refresh_view()
141
+
142
+ return box
143
+
144
+ def collect_params(self) -> dict:
145
+ return {
146
+ "dim_mode": str(self.dim_mode.currentData() or "2d"),
147
+ "dim": int(self.dim.value()),
148
+ "dim1": int(self.dim1.value()),
149
+ "dim2": int(self.dim2.value()),
150
+ "mode": str(self.mode.currentText()),
151
+ "top_percent": float(self.top_percent.value()),
152
+ "view": str(self.view.currentData() or "single"),
153
+ "subsample": int(self.subsample.value()),
154
+ "unfold": str(self.unfold.currentText()),
155
+ "skew_show_grid": bool(self.skew_show_grid.isChecked()),
156
+ "skew_tiles": int(self.skew_tiles.value()),
157
+ "neuron_id": int(self.neuron_id.value()),
158
+ "enable_score": bool(self.enable_score.isChecked()),
159
+ "top_k": int(self.top_k.value()),
160
+ "use_best": bool(self.use_best.isChecked()),
161
+ }
162
+
163
+ def apply_ranges(self, neuron_count: int | None, total_steps: int | None) -> None:
164
+ if neuron_count is None:
165
+ return
166
+ self.neuron_id.setRange(0, max(0, neuron_count - 1))
167
+ self.top_k.setMaximum(max(1, neuron_count))
168
+ if self.top_k.value() <= 0:
169
+ self.top_k.setValue(min(10, neuron_count))
170
+
171
+ def _refresh_dim_mode(self) -> None:
172
+ mode = str(self.dim_mode.currentData() or "2d")
173
+ is_1d = mode == "1d"
174
+ self._dims1d_label.setVisible(is_1d)
175
+ self._dims1d_wrap.setVisible(is_1d)
176
+ self._dims2d_label.setVisible(not is_1d)
177
+ self._dims2d_wrap.setVisible(not is_1d)
178
+ self.unfold.setEnabled(not is_1d)
179
+ self.skew_show_grid.setEnabled(not is_1d)
180
+ self.skew_tiles.setEnabled(not is_1d)
181
+
182
+ def _refresh_view(self) -> None:
183
+ view_single = str(self.view.currentData() or "single") == "single"
184
+ self.neuron_id.setEnabled(view_single)
185
+ self.btn_prev.setEnabled(view_single)
186
+ self.btn_next.setEnabled(view_single)
187
+ self.btn_show.setEnabled(view_single)
188
+ self.use_best.setEnabled(bool(self.enable_score.isChecked()))
189
+ self.top_k.setEnabled(bool(self.enable_score.isChecked()))
190
+
191
+ def _shift(self, delta: int) -> None:
192
+ val = self.neuron_id.value() + int(delta)
193
+ val = max(self.neuron_id.minimum(), min(self.neuron_id.maximum(), val))
194
+ self.neuron_id.setValue(val)
@@ -0,0 +1,52 @@
1
+ """Decode 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 DecodeMode(AbstractAnalysisMode):
12
+ name = "decode"
13
+ display_name = "Decode"
14
+
15
+ def create_params_widget(self) -> QGroupBox:
16
+ box = QGroupBox("Decode Parameters")
17
+ form = QFormLayout(box)
18
+ configure_form_layout(form)
19
+
20
+ self.decode_version = PopupComboBox()
21
+ self.decode_version.addItems(["v2", "v0"])
22
+
23
+ self.num_circ = QSpinBox()
24
+ self.num_circ.setRange(1, 10)
25
+ self.num_circ.setValue(2)
26
+
27
+ self.real_ground = QCheckBox()
28
+ self.real_ground.setChecked(True)
29
+
30
+ self.real_of = QCheckBox()
31
+ self.real_of.setChecked(True)
32
+
33
+ form.addRow("decode_version", self.decode_version)
34
+ form.addRow("num_circ", self.num_circ)
35
+ form.addRow("real_ground", self.real_ground)
36
+ form.addRow("real_of", self.real_of)
37
+
38
+ return box
39
+
40
+ def collect_params(self) -> dict:
41
+ return {
42
+ "decode_version": str(self.decode_version.currentText()),
43
+ "num_circ": int(self.num_circ.value()),
44
+ "real_ground": bool(self.real_ground.isChecked()),
45
+ "real_of": bool(self.real_of.isChecked()),
46
+ }
47
+
48
+ def apply_preset(self, preset: str) -> None:
49
+ if preset == "grid":
50
+ self.num_circ.setValue(2)
51
+ elif preset == "hd":
52
+ self.num_circ.setValue(1)
@@ -0,0 +1,81 @@
1
+ """FR analysis mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from PySide6.QtWidgets import QFormLayout, QGroupBox, QSpinBox
6
+
7
+ from ..views.widgets.popup_combo import PopupComboBox
8
+ from .base import AbstractAnalysisMode, configure_form_layout
9
+
10
+
11
+ class FRMode(AbstractAnalysisMode):
12
+ name = "fr"
13
+ display_name = "FR Heatmap"
14
+
15
+ def create_params_widget(self) -> QGroupBox:
16
+ box = QGroupBox("FR (population heatmap)")
17
+ form = QFormLayout(box)
18
+ configure_form_layout(form)
19
+
20
+ self.neuron_start = QSpinBox()
21
+ self.neuron_start.setRange(0, 1_000_000)
22
+ self.neuron_start.setValue(0)
23
+
24
+ self.neuron_end = QSpinBox()
25
+ self.neuron_end.setRange(0, 1_000_000)
26
+ self.neuron_end.setValue(0)
27
+
28
+ self.time_start = QSpinBox()
29
+ self.time_start.setRange(0, 10_000_000)
30
+ self.time_start.setValue(0)
31
+
32
+ self.time_end = QSpinBox()
33
+ self.time_end.setRange(0, 10_000_000)
34
+ self.time_end.setValue(0)
35
+
36
+ self.normalize = PopupComboBox()
37
+ self.normalize.addItems(["none", "zscore_per_neuron", "minmax_per_neuron"])
38
+ self.normalize.setCurrentText("none")
39
+
40
+ self.mode = PopupComboBox()
41
+ self.mode.addItems(["fr", "spike"])
42
+ self.mode.setToolTip("Use 'fr' for firing-rate matrix (requires preprocessing).")
43
+
44
+ form.addRow("FR neuron_start", self.neuron_start)
45
+ form.addRow("FR neuron_end", self.neuron_end)
46
+ form.addRow("FR t_start", self.time_start)
47
+ form.addRow("FR t_end", self.time_end)
48
+ form.addRow("FR mode", self.mode)
49
+ form.addRow("FR normalize", self.normalize)
50
+
51
+ return box
52
+
53
+ def collect_params(self) -> dict:
54
+ neuron_start = int(self.neuron_start.value())
55
+ neuron_end = int(self.neuron_end.value())
56
+ time_start = int(self.time_start.value())
57
+ time_end = int(self.time_end.value())
58
+ neuron_range = None
59
+ time_range = None
60
+ if neuron_end > neuron_start:
61
+ neuron_range = (neuron_start, neuron_end)
62
+ if time_end > time_start:
63
+ time_range = (time_start, time_end)
64
+ return {
65
+ "neuron_range": neuron_range,
66
+ "time_range": time_range,
67
+ "normalize": str(self.normalize.currentText()),
68
+ "mode": str(self.mode.currentText()),
69
+ }
70
+
71
+ def apply_ranges(self, neuron_count: int | None, total_steps: int | None) -> None:
72
+ if neuron_count is not None:
73
+ self.neuron_start.setRange(0, max(0, neuron_count - 1))
74
+ self.neuron_end.setRange(0, neuron_count)
75
+ if self.neuron_end.value() == 0:
76
+ self.neuron_end.setValue(neuron_count)
77
+ if total_steps is not None:
78
+ self.time_start.setRange(0, max(0, total_steps - 1))
79
+ self.time_end.setRange(0, total_steps)
80
+ if self.time_end.value() == 0:
81
+ self.time_end.setValue(total_steps)
@@ -0,0 +1,92 @@
1
+ """FRM 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
+ QPushButton,
12
+ QSpinBox,
13
+ QWidget,
14
+ )
15
+
16
+ from ..views.widgets.popup_combo import PopupComboBox
17
+ from .base import AbstractAnalysisMode, configure_form_layout
18
+
19
+
20
+ class FRMMode(AbstractAnalysisMode):
21
+ name = "frm"
22
+ display_name = "FRM (single neuron)"
23
+
24
+ def create_params_widget(self) -> QGroupBox:
25
+ box = QGroupBox("FRM (single neuron)")
26
+ form = QFormLayout(box)
27
+ configure_form_layout(form)
28
+
29
+ self.neuron_id = QSpinBox()
30
+ self.neuron_id.setRange(0, 1_000_000)
31
+ self.neuron_id.setValue(0)
32
+
33
+ self.bin_size = QSpinBox()
34
+ self.bin_size.setRange(5, 500)
35
+ self.bin_size.setValue(50)
36
+
37
+ self.min_occupancy = QSpinBox()
38
+ self.min_occupancy.setRange(1, 10_000)
39
+ self.min_occupancy.setValue(1)
40
+
41
+ self.smoothing = QCheckBox()
42
+ self.smoothing.setChecked(False)
43
+
44
+ self.smooth_sigma = QDoubleSpinBox()
45
+ self.smooth_sigma.setRange(0.1, 50.0)
46
+ self.smooth_sigma.setSingleStep(0.1)
47
+ self.smooth_sigma.setValue(1.0)
48
+
49
+ self.mode = PopupComboBox()
50
+ self.mode.addItems(["fr", "spike"])
51
+ self.mode.setToolTip("Use 'fr' for firing-rate matrix (requires preprocessing).")
52
+
53
+ self.btn_prev = QPushButton("←")
54
+ self.btn_next = QPushButton("→")
55
+ neuron_row = QWidget()
56
+ neuron_row_layout = QHBoxLayout(neuron_row)
57
+ neuron_row_layout.setContentsMargins(0, 0, 0, 0)
58
+ neuron_row_layout.addWidget(self.btn_prev)
59
+ neuron_row_layout.addWidget(self.neuron_id, 1)
60
+ neuron_row_layout.addWidget(self.btn_next)
61
+
62
+ self.btn_prev.clicked.connect(lambda: self._shift(-1))
63
+ self.btn_next.clicked.connect(lambda: self._shift(+1))
64
+
65
+ form.addRow("FRM neuron_id", neuron_row)
66
+ form.addRow("FRM bins", self.bin_size)
67
+ form.addRow("FRM min_occupancy", self.min_occupancy)
68
+ form.addRow("FRM smoothing", self.smoothing)
69
+ form.addRow("FRM sigma", self.smooth_sigma)
70
+ form.addRow("FRM mode", self.mode)
71
+
72
+ return box
73
+
74
+ def collect_params(self) -> dict:
75
+ return {
76
+ "neuron_id": int(self.neuron_id.value()),
77
+ "bin_size": int(self.bin_size.value()),
78
+ "min_occupancy": int(self.min_occupancy.value()),
79
+ "smoothing": bool(self.smoothing.isChecked()),
80
+ "smooth_sigma": float(self.smooth_sigma.value()),
81
+ "mode": str(self.mode.currentText()),
82
+ }
83
+
84
+ def apply_ranges(self, neuron_count: int | None, total_steps: int | None) -> None:
85
+ if neuron_count is None:
86
+ return
87
+ self.neuron_id.setRange(0, max(0, neuron_count - 1))
88
+
89
+ def _shift(self, delta: int) -> None:
90
+ val = self.neuron_id.value() + int(delta)
91
+ val = max(self.neuron_id.minimum(), min(self.neuron_id.maximum(), val))
92
+ self.neuron_id.setValue(val)
@@ -0,0 +1,123 @@
1
+ """GridScore analysis modes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from PySide6.QtWidgets import QCheckBox, QDoubleSpinBox, QFormLayout, QGroupBox, QSpinBox
8
+
9
+ from ..views.widgets.popup_combo import PopupComboBox
10
+ from .base import AbstractAnalysisMode, configure_form_layout
11
+
12
+
13
+ class GridScoreMode(AbstractAnalysisMode):
14
+ name = "gridscore"
15
+ display_name = "Grid Score (classic)"
16
+
17
+ def create_params_widget(self) -> QGroupBox:
18
+ box = QGroupBox("Grid Score (classic)")
19
+ form = QFormLayout(box)
20
+ configure_form_layout(form)
21
+
22
+ self.neuron_start = QSpinBox()
23
+ self.neuron_start.setRange(0, 10_000_000)
24
+ self.neuron_start.setValue(0)
25
+
26
+ self.neuron_end = QSpinBox()
27
+ self.neuron_end.setRange(0, 10_000_000)
28
+ self.neuron_end.setValue(0)
29
+
30
+ self.bins = QSpinBox()
31
+ self.bins.setRange(5, 500)
32
+ self.bins.setValue(50)
33
+
34
+ self.min_occupancy = QSpinBox()
35
+ self.min_occupancy.setRange(1, 10_000)
36
+ self.min_occupancy.setValue(1)
37
+
38
+ self.smoothing = QCheckBox()
39
+ self.smoothing.setChecked(False)
40
+
41
+ self.sigma = QDoubleSpinBox()
42
+ self.sigma.setRange(0.1, 50.0)
43
+ self.sigma.setSingleStep(0.1)
44
+ self.sigma.setValue(1.0)
45
+
46
+ self.overlap = QDoubleSpinBox()
47
+ self.overlap.setRange(0.1, 1.0)
48
+ self.overlap.setSingleStep(0.05)
49
+ self.overlap.setValue(0.8)
50
+
51
+ self.mode = PopupComboBox()
52
+ self.mode.addItems(["fr", "spike"])
53
+ self.mode.setToolTip("Use 'fr' for firing-rate maps (requires preprocessing).")
54
+
55
+ self.score_thr = QDoubleSpinBox()
56
+ self.score_thr.setRange(-2.0, 2.0)
57
+ self.score_thr.setSingleStep(0.05)
58
+ self.score_thr.setValue(0.3)
59
+
60
+ self.neuron_id = QSpinBox()
61
+ self.neuron_id.setRange(0, 10_000_000)
62
+ self.neuron_id.setValue(0)
63
+
64
+ form.addRow("neuron_start", self.neuron_start)
65
+ form.addRow("neuron_end", self.neuron_end)
66
+ form.addRow("bins", self.bins)
67
+ form.addRow("min_occupancy", self.min_occupancy)
68
+ form.addRow("smoothing", self.smoothing)
69
+ form.addRow("sigma", self.sigma)
70
+ form.addRow("autocorr overlap", self.overlap)
71
+ form.addRow("mode", self.mode)
72
+ form.addRow("score threshold (viz)", self.score_thr)
73
+
74
+ return box
75
+
76
+ def collect_params(self) -> dict:
77
+ return {
78
+ "gridscore": {
79
+ "neuron_start": int(self.neuron_start.value()),
80
+ "neuron_end": int(self.neuron_end.value()),
81
+ "bins": int(self.bins.value()),
82
+ "min_occupancy": int(self.min_occupancy.value()),
83
+ "smoothing": bool(self.smoothing.isChecked()),
84
+ "sigma": float(self.sigma.value()),
85
+ "overlap": float(self.overlap.value()),
86
+ "mode": str(self.mode.currentText()),
87
+ "score_thr": float(self.score_thr.value()),
88
+ "neuron_id": int(self.neuron_id.value()),
89
+ }
90
+ }
91
+
92
+ def apply_ranges(self, neuron_count: int | None, total_steps: int | None) -> None:
93
+ if neuron_count is None:
94
+ return
95
+ max_neuron = max(0, neuron_count - 1)
96
+ self.neuron_start.setRange(0, max_neuron)
97
+ self.neuron_end.setRange(0, neuron_count)
98
+ if self.neuron_end.value() == 0:
99
+ self.neuron_end.setValue(neuron_count)
100
+ self.neuron_id.setRange(0, max_neuron)
101
+
102
+ def apply_meta(self, meta: dict[str, Any]) -> None:
103
+ if not isinstance(meta, dict):
104
+ return
105
+ if "bins" in meta:
106
+ self.bins.setValue(int(meta["bins"]))
107
+ if "min_occupancy" in meta:
108
+ self.min_occupancy.setValue(int(meta["min_occupancy"]))
109
+ if "smoothing" in meta:
110
+ self.smoothing.setChecked(bool(meta["smoothing"]))
111
+ if "sigma" in meta:
112
+ self.sigma.setValue(float(meta["sigma"]))
113
+ if "overlap" in meta:
114
+ self.overlap.setValue(float(meta["overlap"]))
115
+ if "mode" in meta:
116
+ self.mode.setCurrentText(str(meta["mode"]))
117
+ if "neuron_id" in meta:
118
+ self.neuron_id.setValue(int(meta["neuron_id"]))
119
+
120
+
121
+ class GridScoreInspectMode(GridScoreMode):
122
+ name = "gridscore_inspect"
123
+ display_name = "GridScore Inspect"