canns 0.12.6__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 (64) hide show
  1. canns/__init__.py +39 -3
  2. canns/analyzer/__init__.py +7 -6
  3. canns/analyzer/data/__init__.py +3 -11
  4. canns/analyzer/data/asa/__init__.py +74 -0
  5. canns/analyzer/data/asa/cohospace.py +905 -0
  6. canns/analyzer/data/asa/config.py +246 -0
  7. canns/analyzer/data/asa/decode.py +448 -0
  8. canns/analyzer/data/asa/embedding.py +269 -0
  9. canns/analyzer/data/asa/filters.py +208 -0
  10. canns/analyzer/data/asa/fr.py +439 -0
  11. canns/analyzer/data/asa/path.py +389 -0
  12. canns/analyzer/data/asa/plotting.py +1276 -0
  13. canns/analyzer/data/asa/tda.py +901 -0
  14. canns/analyzer/data/legacy/__init__.py +6 -0
  15. canns/analyzer/data/{cann1d.py → legacy/cann1d.py} +2 -2
  16. canns/analyzer/data/{cann2d.py → legacy/cann2d.py} +3 -3
  17. canns/analyzer/metrics/spatial_metrics.py +70 -100
  18. canns/analyzer/metrics/systematic_ratemap.py +12 -17
  19. canns/analyzer/metrics/utils.py +28 -0
  20. canns/analyzer/model_specific/hopfield.py +19 -16
  21. canns/analyzer/slow_points/checkpoint.py +32 -9
  22. canns/analyzer/slow_points/finder.py +33 -6
  23. canns/analyzer/slow_points/fixed_points.py +12 -0
  24. canns/analyzer/slow_points/visualization.py +22 -10
  25. canns/analyzer/visualization/core/backend.py +15 -26
  26. canns/analyzer/visualization/core/config.py +120 -15
  27. canns/analyzer/visualization/core/jupyter_utils.py +34 -16
  28. canns/analyzer/visualization/core/rendering.py +42 -40
  29. canns/analyzer/visualization/core/writers.py +10 -20
  30. canns/analyzer/visualization/energy_plots.py +78 -28
  31. canns/analyzer/visualization/spatial_plots.py +81 -36
  32. canns/analyzer/visualization/spike_plots.py +27 -7
  33. canns/analyzer/visualization/theta_sweep_plots.py +159 -72
  34. canns/analyzer/visualization/tuning_plots.py +11 -3
  35. canns/data/__init__.py +7 -4
  36. canns/models/__init__.py +10 -0
  37. canns/models/basic/cann.py +102 -40
  38. canns/models/basic/grid_cell.py +9 -8
  39. canns/models/basic/hierarchical_model.py +57 -11
  40. canns/models/brain_inspired/hopfield.py +26 -14
  41. canns/models/brain_inspired/linear.py +15 -16
  42. canns/models/brain_inspired/spiking.py +23 -12
  43. canns/pipeline/__init__.py +4 -8
  44. canns/pipeline/asa/__init__.py +21 -0
  45. canns/pipeline/asa/__main__.py +11 -0
  46. canns/pipeline/asa/app.py +1000 -0
  47. canns/pipeline/asa/runner.py +1095 -0
  48. canns/pipeline/asa/screens.py +215 -0
  49. canns/pipeline/asa/state.py +248 -0
  50. canns/pipeline/asa/styles.tcss +221 -0
  51. canns/pipeline/asa/widgets.py +233 -0
  52. canns/pipeline/gallery/__init__.py +7 -0
  53. canns/task/closed_loop_navigation.py +54 -13
  54. canns/task/open_loop_navigation.py +230 -147
  55. canns/task/tracking.py +156 -24
  56. canns/trainer/__init__.py +8 -5
  57. canns/utils/__init__.py +12 -4
  58. {canns-0.12.6.dist-info → canns-0.13.0.dist-info}/METADATA +6 -3
  59. canns-0.13.0.dist-info/RECORD +91 -0
  60. {canns-0.12.6.dist-info → canns-0.13.0.dist-info}/entry_points.txt +1 -0
  61. canns/pipeline/theta_sweep.py +0 -573
  62. canns-0.12.6.dist-info/RECORD +0 -72
  63. {canns-0.12.6.dist-info → canns-0.13.0.dist-info}/WHEEL +0 -0
  64. {canns-0.12.6.dist-info → canns-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,233 @@
1
+ """Custom Textual widgets for ASA TUI.
2
+
3
+ This module provides reusable UI components for the ASA analysis interface.
4
+ """
5
+
6
+ from pathlib import Path
7
+ import os
8
+ import subprocess
9
+ import sys
10
+
11
+ from rich.ansi import AnsiDecoder
12
+ from rich.text import Text
13
+ from textual.app import ComposeResult
14
+ from textual.containers import Horizontal, ScrollableContainer, Vertical
15
+ from textual.widgets import Button, Input, Label, Static
16
+
17
+
18
+ class ImagePreview(Vertical):
19
+ """Widget for previewing images in the terminal using climage."""
20
+
21
+ DEFAULT_CSS = """
22
+ ImagePreview {
23
+ height: auto;
24
+ min-height: 20;
25
+ border: solid $accent;
26
+ padding: 1;
27
+ }
28
+ #preview-content {
29
+ width: 100%;
30
+ height: auto;
31
+ }
32
+ #preview-scroll {
33
+ height: 1fr;
34
+ }
35
+ #preview-controls Button {
36
+ margin: 0 1 0 0;
37
+ }
38
+ #preview-arrows Button {
39
+ margin: 0 1 0 0;
40
+ }
41
+ #preview-controls, #preview-arrows {
42
+ height: auto;
43
+ }
44
+ """
45
+
46
+ def __init__(self, image_path: Path | None = None, **kwargs):
47
+ super().__init__(**kwargs)
48
+ self.image_path = image_path
49
+ self._zoom_step = 0
50
+
51
+ def compose(self) -> ComposeResult:
52
+ yield Label("Image Preview", id="preview-label")
53
+ yield Static("Path: (none)", id="preview-path")
54
+ yield Input(value="", id="preview-path-input")
55
+ with Horizontal(id="preview-controls"):
56
+ yield Button("Load", id="preview-load-btn")
57
+ yield Button("Open", id="preview-open-btn")
58
+ yield Button("Zoom +", id="preview-zoom-in-btn")
59
+ yield Button("Zoom -", id="preview-zoom-out-btn")
60
+ yield Button("Fit", id="preview-zoom-fit-btn")
61
+ with Horizontal(id="preview-arrows"):
62
+ yield Button("←", id="preview-pan-left-btn")
63
+ yield Button("→", id="preview-pan-right-btn")
64
+ yield Button("↑", id="preview-pan-up-btn")
65
+ yield Button("↓", id="preview-pan-down-btn")
66
+ with ScrollableContainer(id="preview-scroll"):
67
+ yield Static("No image loaded", id="preview-content")
68
+
69
+ def on_button_pressed(self, event: Button.Pressed) -> None:
70
+ if event.button.id == "preview-load-btn":
71
+ raw = self.query_one("#preview-path-input", Input).value.strip()
72
+ if not raw:
73
+ self.update_image(None)
74
+ return
75
+ path = self._resolve_path(raw)
76
+ self.update_image(path)
77
+ elif event.button.id == "preview-open-btn":
78
+ path = self.image_path
79
+ raw = self.query_one("#preview-path-input", Input).value.strip()
80
+ if raw:
81
+ path = self._resolve_path(raw)
82
+ if path is None or not path.exists():
83
+ content = self.query_one("#preview-content", Static)
84
+ content.update("No image to open")
85
+ return
86
+ try:
87
+ if sys.platform == "darwin":
88
+ subprocess.Popen(["open", str(path)])
89
+ elif sys.platform.startswith("win"):
90
+ os.startfile(str(path))
91
+ else:
92
+ subprocess.Popen(["xdg-open", str(path)])
93
+ except Exception as e:
94
+ content = self.query_one("#preview-content", Static)
95
+ content.update(f"Open failed: {e}")
96
+ elif event.button.id == "preview-zoom-in-btn":
97
+ self._zoom_step += 1
98
+ self.update_image(self.image_path)
99
+ elif event.button.id == "preview-zoom-out-btn":
100
+ self._zoom_step -= 1
101
+ self.update_image(self.image_path)
102
+ elif event.button.id == "preview-zoom-fit-btn":
103
+ self._zoom_step = 0
104
+ self.update_image(self.image_path)
105
+ elif event.button.id in {
106
+ "preview-pan-left-btn",
107
+ "preview-pan-right-btn",
108
+ "preview-pan-up-btn",
109
+ "preview-pan-down-btn",
110
+ }:
111
+ scroll = self.query_one("#preview-scroll", ScrollableContainer)
112
+ dx = 0
113
+ dy = 0
114
+ step = 5
115
+ if event.button.id == "preview-pan-left-btn":
116
+ dx = -step
117
+ elif event.button.id == "preview-pan-right-btn":
118
+ dx = step
119
+ elif event.button.id == "preview-pan-up-btn":
120
+ dy = -step
121
+ elif event.button.id == "preview-pan-down-btn":
122
+ dy = step
123
+ scroll.scroll_relative(x=dx, y=dy, animate=False)
124
+
125
+ def on_resize(self, event) -> None:
126
+ if self.image_path and self.image_path.exists():
127
+ self.update_image(self.image_path)
128
+
129
+ def update_image(self, path: Path | None):
130
+ """Update the previewed image."""
131
+ self.image_path = path
132
+ content = self.query_one("#preview-content", Static)
133
+ path_label = self.query_one("#preview-path", Static)
134
+ path_input = self.query_one("#preview-path-input", Input)
135
+
136
+ if path is None or not path.exists():
137
+ content.update("No image loaded")
138
+ path_label.update("Path: (none)")
139
+ path_input.value = ""
140
+ return
141
+ path_label.update(f"Path: {path}")
142
+ path_input.value = str(path)
143
+
144
+ # Try to use climage for terminal preview
145
+ try:
146
+ import climage
147
+
148
+ scroll = self.query_one("#preview-scroll", ScrollableContainer)
149
+ base_width = max(20, scroll.size.width - 4)
150
+ base_height = max(8, scroll.size.height - 2)
151
+ scale = max(0.4, 1 + (self._zoom_step * 0.1))
152
+ width = max(20, int(base_width * scale))
153
+ height = max(8, int(base_height * scale))
154
+ try:
155
+ img_output = climage.convert(str(path), width=width, height=height, is_unicode=True)
156
+ except TypeError:
157
+ img_output = climage.convert(str(path), width=width, is_unicode=True)
158
+ decoder = AnsiDecoder()
159
+ chunks = list(decoder.decode(img_output))
160
+ content.update(Text("\n").join(chunks) if chunks else "")
161
+ except ImportError:
162
+ content.update(f"Image: {path.name}\n(Install climage for preview)")
163
+ except Exception as e:
164
+ content.update(f"Error loading image: {e}")
165
+
166
+ def _resolve_path(self, raw: str) -> Path:
167
+ path = Path(raw).expanduser()
168
+ if path.is_absolute():
169
+ return path
170
+ app = getattr(self, "app", None)
171
+ if app is not None:
172
+ state = getattr(app, "state", None)
173
+ if state is not None and hasattr(state, "workdir"):
174
+ return Path(state.workdir) / path
175
+ return Path.cwd() / path
176
+
177
+
178
+ class ParamGroup(Vertical):
179
+ """Widget for grouping related parameters."""
180
+
181
+ DEFAULT_CSS = """
182
+ ParamGroup {
183
+ border: round $secondary;
184
+ padding: 1;
185
+ margin: 1 0;
186
+ height: auto;
187
+ width: 100%;
188
+ }
189
+ """
190
+
191
+ def __init__(self, title: str, **kwargs):
192
+ super().__init__(**kwargs)
193
+ self.title = title
194
+
195
+ def compose(self) -> ComposeResult:
196
+ yield Label(self.title, classes="param-group-title")
197
+
198
+
199
+ class LogViewer(Vertical):
200
+ """Widget for displaying log messages."""
201
+
202
+ DEFAULT_CSS = """
203
+ LogViewer {
204
+ height: 10;
205
+ border: solid $primary;
206
+ padding: 1;
207
+ overflow-y: scroll;
208
+ }
209
+ """
210
+
211
+ def __init__(self, **kwargs):
212
+ super().__init__(**kwargs)
213
+ self.log_lines = []
214
+
215
+ def compose(self) -> ComposeResult:
216
+ yield Static("", id="log-content")
217
+
218
+ def add_log(self, message: str):
219
+ """Add a log message."""
220
+ self.log_lines.append(message)
221
+ if len(self.log_lines) > 100:
222
+ self.log_lines = self.log_lines[-100:]
223
+
224
+ content = self.query_one("#log-content", Static)
225
+ content.update("\n".join(self.log_lines))
226
+ # Auto-scroll to latest entry
227
+ self.scroll_end(animate=False, immediate=True)
228
+
229
+ def clear(self):
230
+ """Clear all log messages."""
231
+ self.log_lines = []
232
+ content = self.query_one("#log-content", Static)
233
+ content.update("")
@@ -0,0 +1,7 @@
1
+ """Gallery module for model demonstrations and examples.
2
+
3
+ This module will contain interactive TUI demos for CANN models and other
4
+ visualization examples in the future.
5
+ """
6
+
7
+ __all__ = []
@@ -11,11 +11,31 @@ __all__ = [
11
11
 
12
12
 
13
13
  class ClosedLoopNavigationTask(BaseNavigationTask):
14
- """
15
- Closed-loop navigation task that incorporates real-time feedback from a controller.
14
+ """Closed-loop navigation task driven by external control.
15
+
16
+ The agent moves step-by-step using commands supplied at runtime rather than
17
+ following a pre-generated trajectory.
16
18
 
17
- In this task, the agent's movement is controlled step-by-step by external commands
18
- rather than following a pre-generated trajectory.
19
+ Workflow:
20
+ Setup -> Create a task and define environment boundaries.
21
+ Execute -> Call ``step_by_pos`` for each new position.
22
+ Result -> Use geodesic tools or agent history for analysis.
23
+
24
+ Examples:
25
+ >>> from canns.task.closed_loop_navigation import ClosedLoopNavigationTask
26
+ >>>
27
+ >>> task = ClosedLoopNavigationTask(
28
+ ... boundary=[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
29
+ ... dt=0.1,
30
+ ... )
31
+ >>> task.step_by_pos((0.2, 0.2))
32
+ >>> task.set_grid_resolution(0.5, 0.5)
33
+ >>> grid = task.build_movement_cost_grid()
34
+ >>> result = task.compute_geodesic_distance_matrix()
35
+ >>> grid.costs.ndim
36
+ 2
37
+ >>> result.distances.shape[0] == result.distances.shape[1]
38
+ True
19
39
  """
20
40
 
21
41
  def __init__(
@@ -92,11 +112,22 @@ class ClosedLoopNavigationTask(BaseNavigationTask):
92
112
 
93
113
 
94
114
  class TMazeClosedLoopNavigationTask(ClosedLoopNavigationTask):
95
- """
96
- Closed-loop navigation task in a T-maze environment.
115
+ """Closed-loop navigation task in a T-maze environment.
97
116
 
98
- This subclass configures the environment with a T-maze boundary, which is useful
99
- for studying decision-making and spatial navigation in a controlled setting.
117
+ Workflow:
118
+ Setup -> Create a T-maze task.
119
+ Execute -> Step the agent position.
120
+ Result -> Build movement-cost grids or geodesic distances.
121
+
122
+ Examples:
123
+ >>> from canns.task.closed_loop_navigation import TMazeClosedLoopNavigationTask
124
+ >>>
125
+ >>> task = TMazeClosedLoopNavigationTask(dt=0.1)
126
+ >>> task.step_by_pos(task.start_pos)
127
+ >>> task.set_grid_resolution(0.5, 0.5)
128
+ >>> grid = task.build_movement_cost_grid()
129
+ >>> grid.costs.ndim
130
+ 2
100
131
  """
101
132
 
102
133
  def __init__(
@@ -144,12 +175,22 @@ class TMazeClosedLoopNavigationTask(ClosedLoopNavigationTask):
144
175
 
145
176
 
146
177
  class TMazeRecessClosedLoopNavigationTask(TMazeClosedLoopNavigationTask):
147
- """
148
- Closed-loop navigation task in a T-maze environment with recesses at stem-arm junctions.
178
+ """Closed-loop navigation task in a T-maze with recesses at the junction.
179
+
180
+ Workflow:
181
+ Setup -> Create the recess T-maze task.
182
+ Execute -> Step the agent position.
183
+ Result -> Access environment-derived grids for analysis.
149
184
 
150
- This variant adds small rectangular indentations at the T-junction, creating
151
- additional spatial features that may be useful for studying spatial navigation
152
- and decision-making.
185
+ Examples:
186
+ >>> from canns.task.closed_loop_navigation import TMazeRecessClosedLoopNavigationTask
187
+ >>>
188
+ >>> task = TMazeRecessClosedLoopNavigationTask(dt=0.1)
189
+ >>> task.step_by_pos(task.start_pos)
190
+ >>> task.set_grid_resolution(0.5, 0.5)
191
+ >>> grid = task.build_movement_cost_grid()
192
+ >>> grid.costs.ndim
193
+ 2
153
194
  """
154
195
 
155
196
  def __init__(