canns 0.12.7__py3-none-any.whl → 0.13.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 (37) hide show
  1. canns/analyzer/data/__init__.py +3 -11
  2. canns/analyzer/data/asa/__init__.py +74 -0
  3. canns/analyzer/data/asa/cohospace.py +905 -0
  4. canns/analyzer/data/asa/config.py +246 -0
  5. canns/analyzer/data/asa/decode.py +448 -0
  6. canns/analyzer/data/asa/embedding.py +269 -0
  7. canns/analyzer/data/asa/filters.py +208 -0
  8. canns/analyzer/data/asa/fr.py +439 -0
  9. canns/analyzer/data/asa/path.py +389 -0
  10. canns/analyzer/data/asa/plotting.py +1276 -0
  11. canns/analyzer/data/asa/tda.py +901 -0
  12. canns/analyzer/data/legacy/__init__.py +6 -0
  13. canns/analyzer/data/{cann1d.py → legacy/cann1d.py} +2 -2
  14. canns/analyzer/data/{cann2d.py → legacy/cann2d.py} +3 -3
  15. canns/analyzer/visualization/core/backend.py +1 -1
  16. canns/analyzer/visualization/core/config.py +77 -0
  17. canns/analyzer/visualization/core/rendering.py +10 -6
  18. canns/analyzer/visualization/energy_plots.py +22 -8
  19. canns/analyzer/visualization/spatial_plots.py +31 -11
  20. canns/analyzer/visualization/theta_sweep_plots.py +15 -6
  21. canns/pipeline/__init__.py +4 -8
  22. canns/pipeline/asa/__init__.py +21 -0
  23. canns/pipeline/asa/__main__.py +11 -0
  24. canns/pipeline/asa/app.py +1000 -0
  25. canns/pipeline/asa/runner.py +1095 -0
  26. canns/pipeline/asa/screens.py +215 -0
  27. canns/pipeline/asa/state.py +248 -0
  28. canns/pipeline/asa/styles.tcss +221 -0
  29. canns/pipeline/asa/widgets.py +233 -0
  30. canns/pipeline/gallery/__init__.py +7 -0
  31. canns/task/open_loop_navigation.py +3 -1
  32. {canns-0.12.7.dist-info → canns-0.13.0.dist-info}/METADATA +6 -3
  33. {canns-0.12.7.dist-info → canns-0.13.0.dist-info}/RECORD +36 -17
  34. {canns-0.12.7.dist-info → canns-0.13.0.dist-info}/entry_points.txt +1 -0
  35. canns/pipeline/theta_sweep.py +0 -573
  36. {canns-0.12.7.dist-info → canns-0.13.0.dist-info}/WHEEL +0 -0
  37. {canns-0.12.7.dist-info → canns-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,215 @@
1
+ """Modal screens for ASA TUI.
2
+
3
+ This module provides modal overlays for directory selection, help, and error display.
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ from textual.app import ComposeResult
9
+ from textual.containers import Container
10
+ from textual.screen import ModalScreen
11
+ from textual.widgets import Button, DirectoryTree, Label, Static
12
+
13
+
14
+ class WorkdirScreen(ModalScreen[Path]):
15
+ """Modal screen for selecting working directory."""
16
+
17
+ DEFAULT_CSS = """
18
+ WorkdirScreen {
19
+ align: center middle;
20
+ }
21
+
22
+ WorkdirScreen > Container {
23
+ width: 80;
24
+ height: 30;
25
+ border: thick $accent;
26
+ background: $surface;
27
+ }
28
+
29
+ #workdir-tree {
30
+ height: 1fr;
31
+ }
32
+ """
33
+
34
+ def compose(self) -> ComposeResult:
35
+ with Container():
36
+ yield Label("Select Working Directory")
37
+ yield DirectoryTree(Path.home(), id="workdir-tree")
38
+ with Container(id="button-container"):
39
+ yield Button("Select", variant="primary", id="select-btn")
40
+ yield Button("Cancel", id="cancel-btn")
41
+
42
+ def on_button_pressed(self, event: Button.Pressed) -> None:
43
+ if event.button.id == "select-btn":
44
+ tree = self.query_one("#workdir-tree", DirectoryTree)
45
+ if tree.cursor_node:
46
+ selected_path = tree.cursor_node.data.path
47
+ self.dismiss(selected_path)
48
+ elif event.button.id == "cancel-btn":
49
+ self.dismiss(None)
50
+
51
+
52
+ class HelpScreen(ModalScreen):
53
+ """Modal screen showing help and key bindings."""
54
+
55
+ DEFAULT_CSS = """
56
+ HelpScreen {
57
+ align: center middle;
58
+ }
59
+
60
+ HelpScreen > Container {
61
+ width: 70;
62
+ height: 25;
63
+ border: thick $primary;
64
+ background: $surface;
65
+ padding: 2;
66
+ }
67
+
68
+ #help-content {
69
+ overflow-y: scroll;
70
+ }
71
+ """
72
+
73
+ HELP_TEXT = """
74
+ ASA TUI - Terminal User Interface for ASA Analysis
75
+
76
+ KEY BINDINGS:
77
+ Ctrl-W Change working directory
78
+ Ctrl-R Run analysis
79
+ F5 Refresh previews
80
+ ? Show this help
81
+ Esc Quit application
82
+ Tab Navigate between panels
83
+
84
+ ANALYSIS MODULES:
85
+ TDA Topological Data Analysis
86
+ CohoMap Cohomology Map (requires TDA)
87
+ PathCompare Trajectory Comparison (requires CohoMap)
88
+ CohoSpace Cohomology Space Visualization (requires CohoMap)
89
+ FR Firing Rate Heatmap
90
+ FRM Single Neuron Firing Rate Map
91
+ GridScore Grid Cell Analysis
92
+
93
+ WORKFLOW:
94
+ 1. Select working directory (Ctrl-W)
95
+ 2. Choose input mode (ASA or Neuron+Traj)
96
+ 3. Load files
97
+ 4. Configure preprocessing
98
+ 5. Select analysis mode
99
+ 6. Set parameters
100
+ 7. Run analysis (Ctrl-R)
101
+ 8. View results
102
+
103
+ TERMINAL REQUIREMENTS:
104
+ Minimum size: 100 cols × 30 rows
105
+ Recommended: 120 cols × 40 rows
106
+ Tip: Use smaller font size for better display
107
+
108
+ If display is incomplete, try:
109
+ - Reduce terminal font size
110
+ - Maximize terminal window
111
+ - Use fullscreen mode
112
+
113
+ Press any key to close...
114
+ """
115
+
116
+ def compose(self) -> ComposeResult:
117
+ with Container():
118
+ yield Label("Help", id="help-title")
119
+ yield Static(self.HELP_TEXT, id="help-content")
120
+ yield Button("Close", variant="primary", id="close-btn")
121
+
122
+ def on_button_pressed(self, event: Button.Pressed) -> None:
123
+ if event.button.id == "close-btn":
124
+ self.dismiss()
125
+
126
+ def on_key(self, event) -> None:
127
+ self.dismiss()
128
+
129
+
130
+ class ErrorScreen(ModalScreen):
131
+ """Modal screen for displaying errors."""
132
+
133
+ DEFAULT_CSS = """
134
+ ErrorScreen {
135
+ align: center middle;
136
+ }
137
+
138
+ ErrorScreen > Container {
139
+ width: 60;
140
+ height: 20;
141
+ border: thick $error;
142
+ background: $surface;
143
+ padding: 2;
144
+ }
145
+
146
+ #error-message {
147
+ color: $error;
148
+ overflow-y: scroll;
149
+ }
150
+ """
151
+
152
+ def __init__(self, title: str, message: str, **kwargs):
153
+ super().__init__(**kwargs)
154
+ self.error_title = title
155
+ self.error_message = message
156
+
157
+ def compose(self) -> ComposeResult:
158
+ with Container():
159
+ yield Label(self.error_title, id="error-title")
160
+ yield Static(self.error_message, id="error-message")
161
+ yield Button("Close", variant="error", id="close-btn")
162
+
163
+ def on_button_pressed(self, event: Button.Pressed) -> None:
164
+ if event.button.id == "close-btn":
165
+ self.dismiss()
166
+
167
+
168
+ class TerminalSizeWarning(ModalScreen):
169
+ """Warning screen for insufficient terminal size."""
170
+
171
+ DEFAULT_CSS = """
172
+ TerminalSizeWarning {
173
+ align: center middle;
174
+ }
175
+
176
+ TerminalSizeWarning > Container {
177
+ width: 50;
178
+ height: 15;
179
+ border: thick $warning;
180
+ background: $surface;
181
+ padding: 2;
182
+ }
183
+
184
+ #warning-message {
185
+ color: $warning;
186
+ text-align: center;
187
+ }
188
+ """
189
+
190
+ def __init__(self, current_width: int, current_height: int, **kwargs):
191
+ super().__init__(**kwargs)
192
+ self.current_width = current_width
193
+ self.current_height = current_height
194
+
195
+ def compose(self) -> ComposeResult:
196
+ with Container():
197
+ yield Label("⚠ Terminal Size Warning", id="warning-title")
198
+ yield Static(
199
+ f"Current terminal size: {self.current_width} cols × {self.current_height} rows\n\n"
200
+ f"Recommended minimum:\n"
201
+ f"• Width: 100 columns (recommended 120+)\n"
202
+ f"• Height: 30 rows (recommended 40+)\n\n"
203
+ f"Please resize terminal or reduce font size.\n"
204
+ f"Press Esc to continue (may not display properly)",
205
+ id="warning-message",
206
+ )
207
+ yield Button("Continue", variant="warning", id="continue-btn")
208
+
209
+ def on_button_pressed(self, event: Button.Pressed) -> None:
210
+ if event.button.id == "continue-btn":
211
+ self.dismiss()
212
+
213
+ def on_key(self, event) -> None:
214
+ if event.key == "escape":
215
+ self.dismiss()
@@ -0,0 +1,248 @@
1
+ """State management for ASA TUI.
2
+
3
+ This module provides centralized workflow state management with workdir-centric design.
4
+ All file paths are stored relative to the working directory for portability.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ @dataclass
13
+ class WorkflowState:
14
+ """Centralized state for ASA analysis workflow.
15
+
16
+ All file paths are relative to workdir for portability.
17
+ """
18
+
19
+ # Core paths
20
+ workdir: Path = field(default_factory=lambda: Path("."))
21
+
22
+ # Input configuration
23
+ input_mode: str = "asa" # "asa" | "neuron_traj" | "batch"
24
+ preset: str = "grid" # "grid" | "hd" | "none"
25
+
26
+ # File paths (relative to workdir)
27
+ asa_file: Path | None = None
28
+ neuron_file: Path | None = None
29
+ traj_file: Path | None = None
30
+
31
+ # Preprocessing
32
+ preprocess_method: str = "none" # "none" | "embed_spike_trains"
33
+ preprocess_params: dict[str, Any] = field(default_factory=dict)
34
+
35
+ # Analysis configuration
36
+ analysis_mode: str = (
37
+ "tda" # "tda" | "cohomap" | "pathcompare" | "cohospace" | "fr" | "frm" | "gridscore"
38
+ )
39
+ analysis_params: dict[str, Any] = field(default_factory=dict)
40
+
41
+ # Results
42
+ artifacts: dict[str, Path] = field(default_factory=dict)
43
+
44
+ # Runtime state
45
+ is_running: bool = False
46
+ current_stage: str = ""
47
+
48
+
49
+ def relative_path(state: WorkflowState, path: Path) -> Path:
50
+ """Convert absolute path to workdir-relative path.
51
+
52
+ Args:
53
+ state: Current workflow state
54
+ path: Absolute path to convert
55
+
56
+ Returns:
57
+ Path relative to workdir
58
+ """
59
+ try:
60
+ return path.relative_to(state.workdir)
61
+ except ValueError:
62
+ # Path is not relative to workdir, return as-is
63
+ return path
64
+
65
+
66
+ def resolve_path(state: WorkflowState, path: Path | None) -> Path | None:
67
+ """Convert relative path to absolute path.
68
+
69
+ Args:
70
+ state: Current workflow state
71
+ path: Relative path to convert
72
+
73
+ Returns:
74
+ Absolute path or None if path is None
75
+ """
76
+ if path is None:
77
+ return None
78
+
79
+ if path.is_absolute():
80
+ return path
81
+
82
+ return state.workdir / path
83
+
84
+
85
+ def validate_files(state: WorkflowState) -> tuple[bool, str]:
86
+ """Check if required files exist.
87
+
88
+ Args:
89
+ state: Current workflow state
90
+
91
+ Returns:
92
+ Tuple of (is_valid, error_message)
93
+ """
94
+ if state.input_mode == "asa":
95
+ if state.asa_file is None:
96
+ return False, "ASA file not selected"
97
+
98
+ asa_path = resolve_path(state, state.asa_file)
99
+ if not asa_path.exists():
100
+ return False, f"ASA file not found: {asa_path}"
101
+
102
+ # Validate .npz structure
103
+ try:
104
+ import numpy as np
105
+
106
+ data = np.load(asa_path, allow_pickle=True)
107
+ required_keys = ["spike", "t"]
108
+ missing = [k for k in required_keys if k not in data.files]
109
+ if missing:
110
+ return False, f"ASA file missing required keys: {missing}"
111
+ except Exception as e:
112
+ return False, f"Failed to load ASA file: {e}"
113
+
114
+ elif state.input_mode == "neuron_traj":
115
+ if state.neuron_file is None:
116
+ return False, "Neuron file not selected"
117
+ if state.traj_file is None:
118
+ return False, "Trajectory file not selected"
119
+
120
+ neuron_path = resolve_path(state, state.neuron_file)
121
+ traj_path = resolve_path(state, state.traj_file)
122
+
123
+ if not neuron_path.exists():
124
+ return False, f"Neuron file not found: {neuron_path}"
125
+ if not traj_path.exists():
126
+ return False, f"Trajectory file not found: {traj_path}"
127
+
128
+ return True, ""
129
+
130
+
131
+ def get_preset_params(preset: str) -> dict[str, Any]:
132
+ """Load preset configurations for analysis.
133
+
134
+ Args:
135
+ preset: Preset name ("grid", "hd", or "none")
136
+
137
+ Returns:
138
+ Dictionary of preset parameters
139
+ """
140
+ if preset == "grid":
141
+ return {
142
+ "preprocess": {
143
+ "method": "embed_spike_trains",
144
+ "dt": 0.02,
145
+ "sigma": 0.1,
146
+ "speed_filter": False,
147
+ "min_speed": 2.5,
148
+ },
149
+ "tda": {
150
+ "dim": 6,
151
+ "num_times": 5,
152
+ "active_times": 15000,
153
+ "k": 1000,
154
+ "n_points": 1200,
155
+ "metric": "cosine",
156
+ "nbs": 800,
157
+ "maxdim": 1,
158
+ "coeff": 47,
159
+ "do_shuffle": False,
160
+ "num_shuffles": 1000,
161
+ },
162
+ "gridscore": {
163
+ "annulus_inner": 0.3,
164
+ "annulus_outer": 0.7,
165
+ "bin_size": 2.5,
166
+ "smooth_sigma": 2.0,
167
+ },
168
+ }
169
+ elif preset == "hd":
170
+ return {
171
+ "preprocess": {
172
+ "method": "embed_spike_trains",
173
+ "dt": 0.02,
174
+ "sigma": 0.1,
175
+ "speed_filter": False,
176
+ "min_speed": 2.5,
177
+ },
178
+ "tda": {
179
+ "dim": 4,
180
+ "num_times": 5,
181
+ "active_times": 15000,
182
+ "k": 800,
183
+ "n_points": 1000,
184
+ "metric": "cosine",
185
+ "nbs": 600,
186
+ "maxdim": 1,
187
+ "coeff": 47,
188
+ "do_shuffle": False,
189
+ "num_shuffles": 1000,
190
+ },
191
+ }
192
+ else: # "none"
193
+ return {}
194
+
195
+
196
+ def check_cached_artifacts(state: WorkflowState, stage: str) -> bool:
197
+ """Check if stage artifacts exist and are valid.
198
+
199
+ Args:
200
+ state: Current workflow state
201
+ stage: Analysis stage name
202
+
203
+ Returns:
204
+ True if cached artifacts exist and are valid
205
+ """
206
+ stage_dir = state.workdir / stage
207
+ if not stage_dir.exists():
208
+ return False
209
+
210
+ # Define required files for each stage
211
+ stage_artifacts = {
212
+ "TDA": ["barcode.png", "persistence.npz"],
213
+ "CohoMap": ["decoding.npz", "cohomap.png"],
214
+ "PathCompare": ["path_compare.png"],
215
+ "CohoSpace": ["cohospace_trajectory.png"],
216
+ "FR": ["fr_heatmap.png"],
217
+ "FRM": [], # Dynamic based on neuron_id
218
+ "GridScore": ["gridscore_distribution.png", "gridscore.npz"],
219
+ }
220
+
221
+ required_files = stage_artifacts.get(stage, [])
222
+ return all((stage_dir / f).exists() for f in required_files)
223
+
224
+
225
+ def load_cached_result(state: WorkflowState, stage: str) -> dict[str, Any]:
226
+ """Load cached results from previous run.
227
+
228
+ Args:
229
+ state: Current workflow state
230
+ stage: Analysis stage name
231
+
232
+ Returns:
233
+ Dictionary of cached data
234
+ """
235
+ import numpy as np
236
+
237
+ stage_dir = state.workdir / stage
238
+ result = {}
239
+
240
+ # Load .npz files
241
+ for npz_file in stage_dir.glob("*.npz"):
242
+ try:
243
+ data = np.load(npz_file, allow_pickle=True)
244
+ result[npz_file.stem] = dict(data)
245
+ except Exception:
246
+ pass
247
+
248
+ return result
@@ -0,0 +1,221 @@
1
+ /* ASA TUI Styles */
2
+
3
+ /* Hidden class */
4
+ .hidden {
5
+ display: none;
6
+ }
7
+
8
+ /* Main layout - three columns */
9
+ #main-container {
10
+ layout: horizontal;
11
+ height: 100%;
12
+ }
13
+
14
+ /* Left panel - narrow action panel */
15
+ #left-panel {
16
+ width: 22;
17
+ min-width: 20;
18
+ max-width: 26;
19
+ height: 100%; /* CRITICAL: Fill parent height */
20
+ border-right: solid $primary;
21
+ padding: 1;
22
+ }
23
+
24
+ /* Scrollable controls area - fill remaining height to enable scrolling */
25
+ #controls-scroll {
26
+ height: 1fr;
27
+ overflow-y: auto;
28
+ scrollbar-size-vertical: 1;
29
+ min-width: 100%;
30
+ padding-bottom: 2;
31
+ }
32
+
33
+ /* Child containers expand naturally to allow scrolling */
34
+ #preprocess-controls,
35
+ #analysis-controls {
36
+ height: auto !important;
37
+ width: 100%;
38
+ }
39
+
40
+ /* Middle panel - parameters + file browser */
41
+ #middle-panel {
42
+ layout: horizontal;
43
+ width: 0.5fr;
44
+ min-width: 30;
45
+ border-right: solid $primary;
46
+ padding: 0;
47
+ }
48
+
49
+ #params-panel {
50
+ width: 0.7fr;
51
+ padding: 1;
52
+ border-right: solid $secondary;
53
+ height: 100%;
54
+ }
55
+
56
+ #params-header {
57
+ background: $boost;
58
+ padding: 1;
59
+ margin-bottom: 1;
60
+ text-style: bold;
61
+ }
62
+
63
+ #file-tree-panel {
64
+ width: 0.7fr;
65
+ padding: 1;
66
+ height: 100%;
67
+ }
68
+
69
+ #files-header {
70
+ background: $boost;
71
+ padding: 1;
72
+ margin-bottom: 1;
73
+ text-style: bold;
74
+ }
75
+
76
+ #right-panel {
77
+ width: 1.5fr;
78
+ padding: 1;
79
+ height: 100%;
80
+ }
81
+
82
+ /* Results tabs take 75% of right panel height */
83
+ #results-tabs {
84
+ height: 5fr;
85
+ }
86
+
87
+ /* Log viewer at bottom - 25% of right panel height */
88
+ #log-viewer {
89
+ height: 1fr;
90
+ border: solid $primary;
91
+ padding: 1;
92
+ }
93
+
94
+ /* Workdir label */
95
+ #workdir-label {
96
+ background: $boost;
97
+ padding: 1;
98
+ margin-bottom: 1;
99
+ }
100
+
101
+ /* Parameter groups */
102
+ ParamGroup {
103
+ border: round $secondary;
104
+ padding: 1;
105
+ padding-bottom: 1;
106
+ margin: 1 0;
107
+ height: auto;
108
+ width: 100%;
109
+ min-height: 10;
110
+ }
111
+
112
+ .param-group-title {
113
+ text-style: bold;
114
+ color: $accent;
115
+ dock: top;
116
+ }
117
+
118
+ /* Embedding parameters container */
119
+ #emb-params {
120
+ margin-top: 1;
121
+ margin-bottom: 1;
122
+ height: auto !important;
123
+ }
124
+
125
+ #emb-params Label {
126
+ margin-top: 1;
127
+ }
128
+
129
+ #emb-params Input {
130
+ margin-bottom: 1;
131
+ }
132
+
133
+ #emb-params Checkbox {
134
+ margin: 1 0; /* Changed from 0.5 to 1 */
135
+ }
136
+
137
+ /* Buttons */
138
+ Button {
139
+ margin: 1 0;
140
+ }
141
+
142
+ #actions-bar {
143
+ width: 100%;
144
+ margin: 0 0 1 0;
145
+ }
146
+
147
+ #actions-bar Button {
148
+ width: 100%;
149
+ margin: 0 0 1 0;
150
+ }
151
+
152
+ #run-btn {
153
+ width: 100%;
154
+ margin-top: 2;
155
+ }
156
+
157
+ /* Progress bar */
158
+ #progress-bar {
159
+ margin: 1 0;
160
+ }
161
+
162
+ #run-status {
163
+ margin-top: 1;
164
+ }
165
+
166
+ /* Image preview - allow more space */
167
+ ImagePreview {
168
+ height: 1fr;
169
+ min-height: 20;
170
+ border: solid $accent;
171
+ padding: 1;
172
+ }
173
+
174
+ #preview-path {
175
+ color: $text-muted;
176
+ margin-bottom: 1;
177
+ }
178
+
179
+ #preview-path-input {
180
+ margin-bottom: 1;
181
+ }
182
+
183
+ /* File tree - compact for small terminals, now full height */
184
+ #file-tree {
185
+ height: 1fr; /* Fill available space */
186
+ border: solid $secondary;
187
+ }
188
+
189
+ /* Status indicators */
190
+ .running {
191
+ background: $warning;
192
+ }
193
+
194
+ .success {
195
+ background: $success;
196
+ }
197
+
198
+ .error {
199
+ background: $error;
200
+ }
201
+
202
+ /* Tab content */
203
+ TabbedContent {
204
+ height: 1fr;
205
+ }
206
+
207
+ TabPane {
208
+ padding: 1;
209
+ height: 100%;
210
+ }
211
+
212
+ TabbedContent > ContentSwitcher {
213
+ height: 1fr;
214
+ }
215
+
216
+ /* Result status */
217
+ #result-status {
218
+ margin-top: 2;
219
+ padding: 1;
220
+ border: solid $primary;
221
+ }