1717class Application ():
1818 def __init__ (self , network : GUINetwork , width = 1400 , height = 900 , title = "BindsNET GUI" ,
1919 header : str | None = None ,
20- step_rate : int | str = 500 , draw_fps : float | None = None ,
20+ max_steps_per_second : int | float | str | None = None ,
21+ draw_fps : float | None = None ,
2122 parameters : dict | None = None ):
2223 self .width , self .height = width , height
2324 self .network = network
@@ -28,7 +29,14 @@ def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET
2829 self .inputs = None # Set when run() is called; Inputs into network during runtime
2930 self .runtime = None # Set when run() is called; Total runtime of network simulation
3031 self .current_time = 0 # Current timestep in network; incremented during runtime
31- self .step_rate = 1 / step_rate
32+
33+ # Cap on how many sim steps run per wall-clock second. `inf` (or "inf"/"max")
34+ # means "go as fast as possible" -- a 0s timer interval, i.e. one step per tick
35+ # with no throttle. Defaults to the draw rate so, out of the box, the sim steps
36+ # roughly in lock-step with the redraws (fall back to 60 if neither is set).
37+ if max_steps_per_second is None :
38+ max_steps_per_second = draw_fps if draw_fps is not None else 60
39+ self .max_steps_per_second = self ._coerce_sps (max_steps_per_second )
3240
3341 # Decouple the (expensive) full-canvas redraw from the simulation rate: run the
3442 # sim + cheap per-step data capture (widget.capture) every step, but redraw
@@ -49,6 +57,10 @@ def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET
4957 self .step_budget = 0
5058 self ._was_active = None # last active-state, to fire play<->pause transitions once
5159
60+ # Rolling measurement of the ACTUAL steps/second, reported to the panel ~2x/sec.
61+ self ._sps_count = 0 # steps taken since the last measurement window opened
62+ self ._sps_t0 = None # perf_counter at the start of the current window
63+
5264 # Initialize VisPy canvas (GLFW) and grid layout for widget rendering.
5365 self .canvas = scene .SceneCanvas (
5466 title = title ,
@@ -62,14 +74,17 @@ def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET
6274 # spacer row so the topmost axis tick labels aren't clipped by the canvas edge.
6375 self .layout = self .canvas .central_widget .add_grid (margin = 0 )
6476 next_row = 0
77+ # Top padding above everything, so the header (or the topmost tick labels when
78+ # there's no header) isn't flush against the canvas edge.
79+ self .layout .add_widget (row = next_row , col = 0 ).height_max = 24
80+ next_row += 1
6581 if header is not None :
6682 self .title_label = scene .Label (header , color = 'white' , font_size = 20 , bold = True )
6783 self .title_label .height_max = 48
6884 self .layout .add_widget (self .title_label , row = next_row , col = 0 )
85+ next_row += 1
6986 else :
7087 self .title_label = None
71- self .layout .add_widget (row = next_row , col = 0 ).height_max = 12 # top padding
72- next_row += 1
7388
7489 self .grid = self .layout .add_grid (row = next_row , col = 0 , margin = 10 )
7590
@@ -80,6 +95,31 @@ def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET
8095 # toggle_play/step_once/run_n and reads back via set_time etc.
8196 self .panel = QtControlPanel (self , parameters = self .parameters )
8297
98+ # --- steps-per-second rate -------------------------------------------------
99+ @staticmethod
100+ def _coerce_sps (value : int | float | str ) -> float :
101+ # Accept a number, or the words "inf"/"max"/"unlimited"/"" for "as fast as
102+ # possible". Returns a positive float (possibly math.inf).
103+ if isinstance (value , str ):
104+ if value .strip ().lower () in ("" , "inf" , "max" , "unlimited" ):
105+ return float ("inf" )
106+ value = float (value )
107+ value = float (value )
108+ if value <= 0 :
109+ raise ValueError (f"max_steps_per_second must be > 0, got { value } " )
110+ return value
111+
112+ @staticmethod
113+ def _interval_for (sps : float ) -> float :
114+ # inf steps/sec -> a 0s timer interval (vispy fires it as fast as it can).
115+ return 0.0 if sps == float ("inf" ) else 1.0 / sps
116+
117+ def set_max_steps_per_second (self , value : int | float | str ):
118+ # Update the cap live; the running timer's interval is swapped in place.
119+ self .max_steps_per_second = self ._coerce_sps (value )
120+ if hasattr (self , "timer" ):
121+ self .timer .interval = self ._interval_for (self .max_steps_per_second )
122+
83123 def add_widget (self , widget : AbstractWidget , row : int , col : int ):
84124 self .widgets .append (widget )
85125 self .grid .add_widget (widget .grid , row , col )
@@ -111,6 +151,8 @@ def reset(self):
111151 self .step_budget = 0
112152 self ._was_active = None
113153 self ._last_draw = None
154+ self ._sps_count = 0
155+ self ._sps_t0 = None
114156 self .current_time = 0
115157 self .network .reset_state_variables ()
116158 self .network .reset_history ()
@@ -133,6 +175,18 @@ def _set_active(self, active: bool):
133175 self ._was_active = active
134176
135177 def step (self , event ):
178+ # Measure the actual steps/second over a rolling ~0.5s window and report it to
179+ # the panel. Done first (before any early return) so idle/finished states settle
180+ # back to 0 rather than showing a stale rate.
181+ now = time .perf_counter ()
182+ if self ._sps_t0 is None :
183+ self ._sps_t0 = now
184+ elapsed = now - self ._sps_t0
185+ if elapsed >= 0.5 :
186+ self .panel .set_steps_per_second (self ._sps_count / elapsed )
187+ self ._sps_count = 0
188+ self ._sps_t0 = now
189+
136190 # Check if runtime is over
137191 if self .current_time >= self .runtime :
138192 self .timer .stop ()
@@ -162,6 +216,8 @@ def step(self, event):
162216 for widget in self .widgets :
163217 widget .capture (t )
164218
219+ self ._sps_count += 1 # count actual advancing steps for the rate readout
220+
165221 manual = not self .running # advancing via Step / Run N, not Play
166222 if self .step_budget > 0 :
167223 self .step_budget -= 1
@@ -196,7 +252,9 @@ def run(self, inputs: dict[str, torch.Tensor], runtime: int):
196252 widget .prime (self .network , runtime )
197253 # Start paused: the timer ticks, but the sim only advances once the user hits
198254 # Play / Step / Run N (the plot cameras are interactive while idle).
199- self .timer = app .Timer (interval = self .step_rate , connect = self .step , start = True )
255+ self .timer = app .Timer (
256+ interval = self ._interval_for (self .max_steps_per_second ), connect = self .step ,
257+ start = True )
200258 # A separate ~60 Hz timer pumps the control panel's event loop when it has one
201259 # (the Qt window); the GLFW loop ticks both timers. The plots are unaffected.
202260 if self .panel .needs_pump :
0 commit comments