Skip to content

Commit 0d1fbed

Browse files
committed
More GUI implementations
1 parent ca7a06b commit 0d1fbed

4 files changed

Lines changed: 188 additions & 15 deletions

File tree

‎bindsnet/network/network.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,31 @@ def _map_voltage_history(self, h: dict) -> torch.Tensor:
817817
h['view'] = view
818818
return view
819819

820+
def reset_history(self) -> None:
821+
# language=rst
822+
"""
823+
Zero the spike/voltage history GL buffers and the running voltage min/max,
824+
so a reset clears every recorded sample (not just the live state). Maps each
825+
buffer with CUDA, zeros it in place via the cached torch view, and hands it
826+
back to GL. Safe to call between steps (the timer loop is single-threaded, so
827+
no step is concurrently holding a map).
828+
"""
829+
for h in self._spike_history.values():
830+
view = self._map_history(h)
831+
view.zero_()
832+
(err,) = driver.cuGraphicsUnmapResources(1, h['res'], 0)
833+
if err != 0:
834+
raise RuntimeError(f"unmap spike history (reset) failed: {err}")
835+
for h in self._voltage_history.values():
836+
view = self._map_voltage_history(h)
837+
view.zero_()
838+
(err,) = driver.cuGraphicsUnmapResources(1, h['res'], 0)
839+
if err != 0:
840+
raise RuntimeError(f"unmap voltage history (reset) failed: {err}")
841+
h['vmin'].fill_(float('inf'))
842+
h['vmax'].fill_(float('-inf'))
843+
self._step_t = 0
844+
820845
def step(self, input: Dict[str, torch.Tensor], t: int = None) -> None:
821846
### Simulate network activity for one time step ###
822847
if t is None:

‎bindsnet/rendering/app.py‎

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,28 @@
22
import time
33
import torch
44
from bindsnet.rendering.widgets import AbstractWidget
5+
from bindsnet.rendering.controls import QtControlPanel
56
from bindsnet.network.network import GUINetwork
67

8+
# Plots always render on the GLFW backend. GLFW renders straight to the window (bare
9+
# buffer swap) and drives the sim from a tight poll-loop, so the simulation runs at
10+
# full speed. vispy's Qt backend instead renders via an offscreen QOpenGLWidget that
11+
# Qt composites every frame, and dispatches each sim step through a QTimer + the Qt
12+
# event loop -- both much slower (this is why embedding the canvas in Qt was sluggish
13+
# even at 1:1 DPI). Controls live outside the render path; see `controls` below.
14+
app.use_app('glfw')
15+
716

817
class Application():
918
def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET GUI",
1019
header: str | None = None,
11-
step_rate: int | str = 500, draw_fps: float | None = None):
20+
step_rate: int | str = 500, draw_fps: float | None = None,
21+
parameters: dict | None = None):
1222
self.width, self.height = width, height
1323
self.network = network
24+
# Optional {label: value} map surfaced as editable rows in the control panel's
25+
# Parameters section (for future model-rebuild support).
26+
self.parameters = parameters
1427
self.widgets = []
1528
self.inputs = None # Set when run() is called; Inputs into network during runtime
1629
self.runtime = None # Set when run() is called; Total runtime of network simulation
@@ -25,7 +38,18 @@ def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET
2538
self.draw_fps = draw_fps
2639
self._last_draw = None
2740

28-
# Initialize VisPy canvas and grid layout for widget rendering
41+
# --- Simulation run-state, driven by whichever control panel is active -----
42+
# The timer always ticks; whether a tick advances the sim depends on this state.
43+
# running : continuous play (Play/Pause)
44+
# step_budget : count of discrete steps queued (Step / Run N), consumed one
45+
# per tick even while `running` is False.
46+
# "active" (advancing) == running or step_budget > 0; otherwise the sim idles
47+
# and the plot cameras are handed to the user for zoom/pan.
48+
self.running = False
49+
self.step_budget = 0
50+
self._was_active = None # last active-state, to fire play<->pause transitions once
51+
52+
# Initialize VisPy canvas (GLFW) and grid layout for widget rendering.
2953
self.canvas = scene.SceneCanvas(
3054
title=title,
3155
keys='interactive',
@@ -52,6 +76,10 @@ def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET
5276
# Migrate network tensors to shared OpenGL buffers
5377
network.migrate()
5478

79+
# Build the control surface (a separate Qt window). It calls back into
80+
# toggle_play/step_once/run_n and reads back via set_time etc.
81+
self.panel = QtControlPanel(self, parameters=self.parameters)
82+
5583
def add_widget(self, widget: AbstractWidget, row: int, col: int):
5684
self.widgets.append(widget)
5785
self.grid.add_widget(widget.grid, row, col)
@@ -61,34 +89,95 @@ def add_widget(self, widget: AbstractWidget, row: int, col: int):
6189
if self.runtime is not None:
6290
widget.prime(self.network, self.runtime)
6391

92+
# --- Control callbacks (panel-agnostic) ------------------------------------
93+
def toggle_play(self):
94+
self.running = not self.running
95+
self.panel.set_playing(self.running)
96+
97+
def step_once(self):
98+
# Queue a single simulation step; consumed on the next tick even while paused.
99+
self.step_budget += 1
100+
101+
def run_n(self, n: int):
102+
# Queue N steps.
103+
if n and n > 0:
104+
self.step_budget += int(n)
105+
106+
def reset(self):
107+
# Clear the network's live state AND its recorded GL history, rewind to t=0,
108+
# restore every plot's initial view, and re-arm the run-state machine. The timer
109+
# is restarted in case the run had already finished (step() stops it at the end).
110+
self.running = False
111+
self.step_budget = 0
112+
self._was_active = None
113+
self._last_draw = None
114+
self.current_time = 0
115+
self.network.reset_state_variables()
116+
self.network.reset_history()
117+
for widget in self.widgets:
118+
widget.reset()
119+
# Re-lock cameras to the running (non-interactive) state via the transition path.
120+
self._set_active(False)
121+
if hasattr(self, "timer") and not self.timer.running:
122+
self.timer.start()
123+
self.panel.on_reset()
124+
self.canvas.update()
125+
126+
def _set_active(self, active: bool):
127+
# Fire on each play<->pause transition: lock the cameras (and resume the
128+
# follow window) while advancing, hand them back for zoom/pan while idle.
129+
if active == self._was_active:
130+
return
131+
for widget in self.widgets:
132+
widget.set_paused(not active)
133+
self._was_active = active
134+
64135
def step(self, event):
65136
# Check if runtime is over
66137
if self.current_time >= self.runtime:
67138
self.timer.stop()
68139
# Hand the (bounded) cameras to the user now that the follow window is done.
69140
for widget in self.widgets:
70141
widget.finish()
142+
self.running = False
143+
self.panel.on_finish()
71144
self.canvas.update()
72145
return
73146

147+
# Advance only when playing or there are queued steps; otherwise idle so the
148+
# user can zoom/pan the paused plots.
149+
active = self.running or self.step_budget > 0
150+
self._set_active(active)
151+
if not active:
152+
return
153+
154+
t = self.current_time
155+
74156
# Simulate one timestep in network
75-
tstep_inputs = {layer_name: layer_inputs[self.current_time] for layer_name, layer_inputs in self.inputs.items()}
76-
self.network.step(tstep_inputs, self.current_time)
157+
tstep_inputs = {layer_name: layer_inputs[t] for layer_name, layer_inputs in self.inputs.items()}
158+
self.network.step(tstep_inputs, t)
77159

78160
# Cheap per-step data capture into GPU buffers -- ALWAYS every step, so the data
79161
# is complete regardless of how often we draw.
80162
for widget in self.widgets:
81-
widget.capture(self.current_time)
163+
widget.capture(t)
164+
165+
manual = not self.running # advancing via Step / Run N, not Play
166+
if self.step_budget > 0:
167+
self.step_budget -= 1
82168

83169
# Throttle the expensive part: widget.render() (camera/axes/uniforms) + the
84-
# full-canvas redraw + buffer swap. render() schedules its own redraw, so it is
85-
# gated together with canvas.update().
86-
if self._should_draw():
170+
# full-canvas redraw + buffer swap. Force a draw on the final queued manual step
171+
# so a Step / Run N result is shown immediately rather than at the next throttled
172+
# draw.
173+
force_draw = manual and self.step_budget == 0
174+
if force_draw or self._should_draw():
87175
for widget in self.widgets:
88-
widget.render(self.current_time)
176+
widget.render(t)
89177
self.canvas.update()
90178

91179
self.current_time += 1
180+
self.panel.set_time(self.current_time, self.runtime)
92181

93182
def _should_draw(self):
94183
if self.draw_fps is None:
@@ -105,5 +194,12 @@ def run(self, inputs: dict[str, torch.Tensor], runtime: int):
105194
# Prime widgets now that runtime is known (full-history buffers need it).
106195
for widget in self.widgets:
107196
widget.prime(self.network, runtime)
197+
# Start paused: the timer ticks, but the sim only advances once the user hits
198+
# Play / Step / Run N (the plot cameras are interactive while idle).
108199
self.timer = app.Timer(interval=self.step_rate, connect=self.step, start=True)
200+
# A separate ~60 Hz timer pumps the control panel's event loop when it has one
201+
# (the Qt window); the GLFW loop ticks both timers. The plots are unaffected.
202+
if self.panel.needs_pump:
203+
self.pump_timer = app.Timer(interval=1/60, connect=lambda e: self.panel.pump(), start=True)
109204
app.run()
205+
self.panel.shutdown()

‎bindsnet/rendering/widgets.py‎

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,23 @@ def render(self, t):
9898
# on draw steps (see Application.draw_fps throttling).
9999
pass
100100

101+
def set_paused(self, paused: bool):
102+
# Called by the app on every play<->pause transition. Default: nothing to do.
103+
# Override for widgets that lock the camera while running so it can be handed
104+
# to the user for zoom/pan while paused (see GraphPlotWidget).
105+
pass
106+
101107
def finish(self):
102108
# Called once when the simulation completes. Default: nothing to do. Override
103109
# for widgets that lock interaction during the run (graph plots).
104110
pass
105111

112+
def reset(self):
113+
# Called by the app when the simulation is reset (network state cleared, time
114+
# back to 0). Default: nothing to do. Override to restore the initial camera
115+
# view; the underlying GL history buffers are cleared by GUINetwork.reset_history.
116+
pass
117+
106118

107119
# A plotting widget with x and y axis
108120
class GraphPlotWidget(AbstractWidget):
@@ -125,6 +137,22 @@ def __init__(self, link_x=True, link_y=True):
125137
if link_x: self.x_axis.link_view(self.view) # follows camera
126138
if link_y: self.y_axis.link_view(self.view)
127139

140+
self._initial_rect = None # starting follow window, captured in each prime()
141+
142+
def reset(self):
143+
# Restore the starting follow window and re-lock the camera; the sim advances
144+
# again from t=0 on the next Play/Step (set_paused re-locks too, but reset may
145+
# happen after finish() unlocked it).
146+
if self._initial_rect is not None:
147+
self.view.camera.rect = self._initial_rect
148+
self.view.camera.interactive = False
149+
150+
def set_paused(self, paused: bool):
151+
# While paused, hand the (bounded) camera to the user for zoom/pan; while
152+
# running, lock it so render()'s per-draw follow window isn't fought by user
153+
# input. limit_rect still keeps zoom/pan inside the plotted data.
154+
self.view.camera.interactive = paused
155+
128156
def finish(self):
129157
# Sim done: hand the camera to the user (still bounded by limit_rect).
130158
self.view.camera.interactive = True
@@ -192,7 +220,8 @@ def prime(self, network, runtime=None):
192220
self.view.camera.limit_rect = Rect(0, y0, self.total_timesteps, y1 - y0)
193221
# Start showing the first trailing window; absolute-time x means zoom-out
194222
# reveals all of history. y grows later as extremes appear (see render).
195-
self.view.camera.rect = (0, y0, self.max_timesteps, y1 - y0)
223+
self._initial_rect = (0, y0, self.max_timesteps, y1 - y0)
224+
self.view.camera.rect = self._initial_rect
196225
self.y_axis.axis.axis_label = "Voltage (mV)"
197226
self.x_axis.axis.axis_label = "Timestep"
198227
# Ticks at constant multiples of `step` so a sliding domain produces smoothly
@@ -272,7 +301,8 @@ def prime(self, network, runtime=None):
272301
self.view.camera.limit_rect = Rect(0, 0, self.total_timesteps, self.layer_size)
273302
# Start showing the first trailing window; absolute-time x means zoom-out
274303
# reveals all of history.
275-
self.view.camera.rect = (0, 0, self.max_timesteps, self.layer_size)
304+
self._initial_rect = (0, 0, self.max_timesteps, self.layer_size)
305+
self.view.camera.rect = self._initial_rect
276306
self.y_axis.axis.axis_label = "Neuron"
277307
self.x_axis.axis.axis_label = "Timestep"
278308
# Ticks at constant multiples of `step` so a sliding domain produces smoothly
@@ -351,7 +381,8 @@ def prime(self, network, runtime=None):
351381
self.view.add(self.visual)
352382
# Bound zoom/pan to the matrix extent (target x source neurons).
353383
self.view.camera.limit_rect = Rect(0, 0, cols, rows)
354-
self.view.camera.rect = (0, 0, cols, rows)
384+
self._initial_rect = (0, 0, cols, rows)
385+
self.view.camera.rect = self._initial_rect
355386
# Unlike the time-series plots, the heatmap has no follow window fighting the
356387
# user, so allow free (bounded) zoom/pan for the whole run.
357388
self.view.camera.interactive = True
@@ -370,6 +401,18 @@ def _add_colorbar(self, clim):
370401
)
371402
self.grid.add_widget(self.colorbar, row=0, col=2).width_max = 95
372403

404+
def set_paused(self, paused: bool):
405+
# The heatmap has no follow window fighting the user, so its camera is left
406+
# interactive for the whole run (see prime); pausing changes nothing here.
407+
pass
408+
409+
def reset(self):
410+
# The heatmap camera stays interactive the whole run (no follow window); just
411+
# restore the initial full-matrix view and re-show the (now-cleared) values.
412+
if self._initial_rect is not None:
413+
self.view.camera.rect = self._initial_rect
414+
self.visual.migrate()
415+
373416
def render(self, t):
374417
if t % self.refresh_every == 0:
375418
self.visual.migrate()

‎examples/rendering/main.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
SIM_TIME = 1000
77
BATCH_SIZE = 1
88
DEVICE = "cuda"
9-
DRAW_FPS = 50 # cap plot redraws; the sim runs as fast as it can between draws
9+
DRAW_FPS = 30 # cap plot redraws; the sim runs as fast as it can between draws
1010

1111
IN_SIZE = 100
1212
EXC_SIZE = 20_000
@@ -26,8 +26,17 @@
2626
INH_TO_EXC_CONNECTIVITY,
2727
EXC_TO_INH_CONNECTIVITY,
2828
)
29-
app = Application(network, 1400, 900, header="BindsNET Network Activity",
30-
step_rate=99999999999, draw_fps=DRAW_FPS)
29+
app = Application(network, 2800, 1800, header="BindsNET Network Activity",
30+
step_rate=99999999999, draw_fps=DRAW_FPS,
31+
parameters={
32+
"Input size": IN_SIZE,
33+
"Excitatory size": EXC_SIZE,
34+
"Inhibitory size": INH_SIZE,
35+
"I -> EXC connectivity": I_TO_EXC_CONNECTIVITY,
36+
"I -> INH connectivity": I_TO_INH_CONNECTIVITY,
37+
"INH -> EXC connectivity": INH_TO_EXC_CONNECTIVITY,
38+
"EXC -> INH connectivity": EXC_TO_INH_CONNECTIVITY,
39+
})
3140
inputs = {"I": torch.rand(SIM_TIME, BATCH_SIZE, IN_SIZE, device=DEVICE) > 0.90}
3241
app.add_widget(
3342
RasterPlot(layer_name="EXC_LIF", max_timesteps=500),

0 commit comments

Comments
 (0)