|
15 | 15 |
|
16 | 16 |
|
17 | 17 | class Application(): |
18 | | - def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET GUI", |
| 18 | + def __init__(self, network: GUINetwork, |
| 19 | + width=1400, height=900, title="BindsNET GUI", |
19 | 20 | header: str | None = None, |
20 | 21 | max_steps_per_second: int | float | str | None = None, |
21 | 22 | draw_fps: float | None = None, |
22 | 23 | parameters: dict | None = None): |
23 | 24 | self.width, self.height = width, height |
| 25 | + |
| 26 | + # `network` is a GUINetwork. If it OVERRIDES build() (an inheritable model |
| 27 | + # definition), the Application drives its lifecycle: build() assembles it, |
| 28 | + # make_input() supplies the stimulus, network.parameters feeds the editable panel, |
| 29 | + # and the "Apply & Reload" button rebuilds it in place (see reload_model). A plain |
| 30 | + # GUINetwork assembled imperatively (no build() override) is used as-is with no |
| 31 | + # reload, driven by the `inputs` dict passed to run(). |
24 | 32 | self.network = network |
25 | | - # Optional {label: value} map surfaced as editable rows in the control panel's |
26 | | - # Parameters section (for future model-rebuild support). |
27 | | - self.parameters = parameters |
| 33 | + self._buildable = type(network).build is not GUINetwork.build |
| 34 | + self._has_make_input = type(network).make_input is not GUINetwork.make_input |
| 35 | + self.can_reload = self._buildable |
| 36 | + if self._buildable: |
| 37 | + self.network.rebuild() # initial build() from constructor params |
| 38 | + self.parameters = dict(network.parameters) |
| 39 | + else: |
| 40 | + self.parameters = parameters # legacy: cosmetic-only panel rows |
28 | 41 | self.widgets = [] |
29 | 42 | self.inputs = None # Set when run() is called; Inputs into network during runtime |
30 | 43 | self.runtime = None # Set when run() is called; Total runtime of network simulation |
@@ -89,7 +102,7 @@ def __init__(self, network: GUINetwork, width=1400, height=900, title="BindsNET |
89 | 102 | self.grid = self.layout.add_grid(row=next_row, col=0, margin=10) |
90 | 103 |
|
91 | 104 | # Migrate network tensors to shared OpenGL buffers |
92 | | - network.migrate() |
| 105 | + self.network.migrate() |
93 | 106 |
|
94 | 107 | # Build the control surface (a separate Qt window). It calls back into |
95 | 108 | # toggle_play/step_once/run_n and reads back via set_time etc. |
@@ -165,6 +178,98 @@ def reset(self): |
165 | 178 | self.panel.on_reset() |
166 | 179 | self.canvas.update() |
167 | 180 |
|
| 181 | + def reload_model(self): |
| 182 | + # language=rst |
| 183 | + """ |
| 184 | + Rebuild the network from the control panel's current parameters and re-bind the |
| 185 | + plots in place, WITHOUT recreating the canvas / control window (that is what causes |
| 186 | + the black-screen/lag we avoid). Driven by the panel's "Apply & Reload" button, which |
| 187 | + fires on the main GL thread during panel.pump() -- the same path reset() uses to |
| 188 | + touch GL, so the context is current and these calls are safe. |
| 189 | +
|
| 190 | + The network's :meth:`GUINetwork.rebuild` reassembles the model IN PLACE (frees the |
| 191 | + old GL buffers, tears down the layers, re-runs build() with the edited parameters), |
| 192 | + so the same network object is kept and the widgets simply re-bind to it. |
| 193 | + """ |
| 194 | + if not self.can_reload: |
| 195 | + return |
| 196 | + if self.runtime is None: |
| 197 | + self.panel.show_status("Start the run before reloading.", error=True) |
| 198 | + return |
| 199 | + |
| 200 | + # Coerce the edited fields first; a bad value aborts before the model is touched. |
| 201 | + try: |
| 202 | + values = self.panel.get_parameter_values() |
| 203 | + except Exception as exc: |
| 204 | + self.panel.show_status(f"Invalid parameter: {exc}", error=True) |
| 205 | + return |
| 206 | + |
| 207 | + self.canvas.set_current() # ensure GL calls below target the plot context |
| 208 | + try: |
| 209 | + self.network.rebuild(**values) # free GL, clear, set params, re-run build() |
| 210 | + self.network.migrate() # allocate the rebuilt net's shared GL buffers |
| 211 | + except Exception as exc: |
| 212 | + # build() failed (e.g. an invalid size). The network is now an empty shell, but |
| 213 | + # rebuild() always clears-then-builds, so a subsequent reload with good values |
| 214 | + # fully recovers. Leave the sim paused and report. |
| 215 | + self.panel.show_status(f"Build failed: {exc}", error=True) |
| 216 | + return |
| 217 | + |
| 218 | + # Re-bind every plot to the rebuilt network (releases old visuals, allocates fresh |
| 219 | + # history buffers). Done before regenerating inputs so a (rare) input mismatch can't |
| 220 | + # leave widgets pointing at freed buffers. |
| 221 | + for widget in self.widgets: |
| 222 | + widget.reload(self.network) |
| 223 | + self.parameters = dict(self.network.parameters) |
| 224 | + |
| 225 | + # Regenerate the stimulus for the rebuilt model. make_input() tracks the parameters |
| 226 | + # so it always fits; a fixed dict is only warned about if it no longer matches. |
| 227 | + input_warning = None |
| 228 | + if self._has_make_input: |
| 229 | + self.inputs = self.network.make_input(self.runtime) |
| 230 | + else: |
| 231 | + try: |
| 232 | + self._validate_inputs(self.inputs, self.network) |
| 233 | + except Exception as exc: |
| 234 | + input_warning = f"Reloaded, but inputs no longer fit: {exc}" |
| 235 | + |
| 236 | + # Re-arm the run-state machine at t=0 (the rebuilt net + its history buffers are |
| 237 | + # already fresh/zeroed, so no reset_state_variables/reset_history needed). |
| 238 | + self.running = False |
| 239 | + self.step_budget = 0 |
| 240 | + self._was_active = None |
| 241 | + self._last_draw = None |
| 242 | + self._sps_count = 0 |
| 243 | + self._sps_t0 = None |
| 244 | + self.current_time = 0 |
| 245 | + self._set_active(False) |
| 246 | + if hasattr(self, "timer") and not self.timer.running: |
| 247 | + self.timer.start() |
| 248 | + |
| 249 | + self.panel.on_reset() |
| 250 | + self.panel.set_parameter_values(self.network.parameters) |
| 251 | + if input_warning is not None: |
| 252 | + self.panel.show_status(input_warning, error=True) |
| 253 | + else: |
| 254 | + self.panel.show_status("Model reloaded.") |
| 255 | + self.canvas.update() |
| 256 | + |
| 257 | + def _validate_inputs(self, inputs: dict, network: GUINetwork): |
| 258 | + # Ensure the (possibly regenerated) inputs match the rebuilt network before swapping |
| 259 | + # anything in: each input tensor's last dim must equal its target layer's n, and a |
| 260 | + # time-major tensor must cover the whole runtime. Raises ValueError on a mismatch. |
| 261 | + for name, tensor in inputs.items(): |
| 262 | + if name not in network.layers: |
| 263 | + continue |
| 264 | + n = int(network.layers[name].n) |
| 265 | + if int(tensor.shape[-1]) != n: |
| 266 | + raise ValueError( |
| 267 | + f"input '{name}' last dim {int(tensor.shape[-1])} != layer '{name}' n {n}. " |
| 268 | + f"Pass `inputs` to run() as a builder function so it tracks the parameters.") |
| 269 | + if tensor.dim() >= 2 and int(tensor.shape[0]) < self.runtime: |
| 270 | + raise ValueError( |
| 271 | + f"input '{name}' has {int(tensor.shape[0])} timesteps < runtime {self.runtime}.") |
| 272 | + |
168 | 273 | def _set_active(self, active: bool): |
169 | 274 | # Fire on each play<->pause transition: lock the cameras (and resume the |
170 | 275 | # follow window) while advancing, hand them back for zoom/pan while idle. |
@@ -244,9 +349,21 @@ def _should_draw(self): |
244 | 349 | return True |
245 | 350 | return False |
246 | 351 |
|
247 | | - def run(self, inputs: dict[str, torch.Tensor], runtime: int): |
248 | | - self.inputs = inputs |
| 352 | + def run(self, inputs: dict[str, torch.Tensor] | None = None, runtime: int = None): |
| 353 | + # The stimulus comes from the network's make_input(runtime) when it implements one |
| 354 | + # (regenerated on every reload so its shape tracks the parameters); otherwise a fixed |
| 355 | + # `inputs` dict must be supplied here. `runtime` sizes the full-history GL buffers. |
| 356 | + if runtime is None: |
| 357 | + raise ValueError("Application.run() requires `runtime`.") |
249 | 358 | self.runtime = runtime |
| 359 | + if self._has_make_input: |
| 360 | + self.inputs = self.network.make_input(runtime) |
| 361 | + elif inputs is not None: |
| 362 | + self.inputs = inputs |
| 363 | + else: |
| 364 | + raise ValueError( |
| 365 | + "Application.run() needs an `inputs` dict unless the network implements " |
| 366 | + "make_input().") |
250 | 367 | # Prime widgets now that runtime is known (full-history buffers need it). |
251 | 368 | for widget in self.widgets: |
252 | 369 | widget.prime(self.network, runtime) |
|
0 commit comments