22import time
33import torch
44from bindsnet .rendering .widgets import AbstractWidget
5+ from bindsnet .rendering .controls import QtControlPanel
56from 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
817class 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 ()
0 commit comments