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.
- canns/__init__.py +39 -3
- canns/analyzer/__init__.py +7 -6
- canns/analyzer/data/__init__.py +3 -11
- canns/analyzer/data/asa/__init__.py +74 -0
- canns/analyzer/data/asa/cohospace.py +905 -0
- canns/analyzer/data/asa/config.py +246 -0
- canns/analyzer/data/asa/decode.py +448 -0
- canns/analyzer/data/asa/embedding.py +269 -0
- canns/analyzer/data/asa/filters.py +208 -0
- canns/analyzer/data/asa/fr.py +439 -0
- canns/analyzer/data/asa/path.py +389 -0
- canns/analyzer/data/asa/plotting.py +1276 -0
- canns/analyzer/data/asa/tda.py +901 -0
- canns/analyzer/data/legacy/__init__.py +6 -0
- canns/analyzer/data/{cann1d.py → legacy/cann1d.py} +2 -2
- canns/analyzer/data/{cann2d.py → legacy/cann2d.py} +3 -3
- canns/analyzer/metrics/spatial_metrics.py +70 -100
- canns/analyzer/metrics/systematic_ratemap.py +12 -17
- canns/analyzer/metrics/utils.py +28 -0
- canns/analyzer/model_specific/hopfield.py +19 -16
- canns/analyzer/slow_points/checkpoint.py +32 -9
- canns/analyzer/slow_points/finder.py +33 -6
- canns/analyzer/slow_points/fixed_points.py +12 -0
- canns/analyzer/slow_points/visualization.py +22 -10
- canns/analyzer/visualization/core/backend.py +15 -26
- canns/analyzer/visualization/core/config.py +120 -15
- canns/analyzer/visualization/core/jupyter_utils.py +34 -16
- canns/analyzer/visualization/core/rendering.py +42 -40
- canns/analyzer/visualization/core/writers.py +10 -20
- canns/analyzer/visualization/energy_plots.py +78 -28
- canns/analyzer/visualization/spatial_plots.py +81 -36
- canns/analyzer/visualization/spike_plots.py +27 -7
- canns/analyzer/visualization/theta_sweep_plots.py +159 -72
- canns/analyzer/visualization/tuning_plots.py +11 -3
- canns/data/__init__.py +7 -4
- canns/models/__init__.py +10 -0
- canns/models/basic/cann.py +102 -40
- canns/models/basic/grid_cell.py +9 -8
- canns/models/basic/hierarchical_model.py +57 -11
- canns/models/brain_inspired/hopfield.py +26 -14
- canns/models/brain_inspired/linear.py +15 -16
- canns/models/brain_inspired/spiking.py +23 -12
- canns/pipeline/__init__.py +4 -8
- canns/pipeline/asa/__init__.py +21 -0
- canns/pipeline/asa/__main__.py +11 -0
- canns/pipeline/asa/app.py +1000 -0
- canns/pipeline/asa/runner.py +1095 -0
- canns/pipeline/asa/screens.py +215 -0
- canns/pipeline/asa/state.py +248 -0
- canns/pipeline/asa/styles.tcss +221 -0
- canns/pipeline/asa/widgets.py +233 -0
- canns/pipeline/gallery/__init__.py +7 -0
- canns/task/closed_loop_navigation.py +54 -13
- canns/task/open_loop_navigation.py +230 -147
- canns/task/tracking.py +156 -24
- canns/trainer/__init__.py +8 -5
- canns/utils/__init__.py +12 -4
- {canns-0.12.6.dist-info → canns-0.13.0.dist-info}/METADATA +6 -3
- canns-0.13.0.dist-info/RECORD +91 -0
- {canns-0.12.6.dist-info → canns-0.13.0.dist-info}/entry_points.txt +1 -0
- canns/pipeline/theta_sweep.py +0 -573
- canns-0.12.6.dist-info/RECORD +0 -72
- {canns-0.12.6.dist-info → canns-0.13.0.dist-info}/WHEEL +0 -0
- {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("")
|
|
@@ -11,11 +11,31 @@ __all__ = [
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class ClosedLoopNavigationTask(BaseNavigationTask):
|
|
14
|
-
"""
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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__(
|