rnba 0.2.1__tar.gz → 0.2.2__tar.gz

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.
Files changed (130) hide show
  1. {rnba-0.2.1 → rnba-0.2.2}/PKG-INFO +3 -1
  2. {rnba-0.2.1 → rnba-0.2.2}/README.md +2 -0
  3. {rnba-0.2.1 → rnba-0.2.2}/pyproject.toml +1 -1
  4. {rnba-0.2.1 → rnba-0.2.2}/rnba/__init__.py +1 -1
  5. {rnba-0.2.1 → rnba-0.2.2}/rnba/visualize_activity_flow.py +209 -39
  6. {rnba-0.2.1 → rnba-0.2.2}/.gitignore +0 -0
  7. {rnba-0.2.1 → rnba-0.2.2}/LICENSE +0 -0
  8. {rnba-0.2.1 → rnba-0.2.2}/NBA24_QA_sentence_input.py +0 -0
  9. {rnba-0.2.1 → rnba-0.2.2}/NBA_SGall_sentence_input.py +0 -0
  10. {rnba-0.2.1 → rnba-0.2.2}/NBA_read_architecture.py +0 -0
  11. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/.gitignore +0 -0
  12. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/CMakeLists.txt +0 -0
  13. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/CMakePresets.json +0 -0
  14. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/README.md +0 -0
  15. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/data/README.md +0 -0
  16. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/data/example_snb1_contract.json +0 -0
  17. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/data/model_smoke_contract.json +0 -0
  18. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/data/network.schema.json +0 -0
  19. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/data/snb15_contract.json +0 -0
  20. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/data/snb20_contract.json +0 -0
  21. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/include/model.hpp +0 -0
  22. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/include/output.hpp +0 -0
  23. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/include/parser.hpp +0 -0
  24. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/include/simulator.hpp +0 -0
  25. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/include/sparse_matrix.hpp +0 -0
  26. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/src/main.cpp +0 -0
  27. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/src/model.cpp +0 -0
  28. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/src/output.cpp +0 -0
  29. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/src/parser.cpp +0 -0
  30. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/src/simulator.cpp +0 -0
  31. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/src/sparse_matrix.cpp +0 -0
  32. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tests/CMakeLists.txt +0 -0
  33. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tests/data/model_smoke_contract.json +0 -0
  34. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tests/data/snb20_contract.json +0 -0
  35. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tests/test_golden.cpp +0 -0
  36. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tests/test_parser.cpp +0 -0
  37. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tests/test_simulator.cpp +0 -0
  38. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tools/export_network.py +0 -0
  39. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tools/plot_cpp_qa.py +0 -0
  40. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tools/plot_ncv_output.py +0 -0
  41. {rnba-0.2.1 → rnba-0.2.2}/cpp-sim/tools/plot_trace_comparison.py +0 -0
  42. {rnba-0.2.1 → rnba-0.2.2}/docs/analysis/QA_GENERATED_SIMULATION_WALKTHROUGH.md +0 -0
  43. {rnba-0.2.1 → rnba-0.2.2}/docs/analysis/README.md +0 -0
  44. {rnba-0.2.1 → rnba-0.2.2}/docs/analysis/collapse_verification.md +0 -0
  45. {rnba-0.2.1 → rnba-0.2.2}/docs/analysis/rowviz.py +0 -0
  46. {rnba-0.2.1 → rnba-0.2.2}/docs/analysis/schematic_inclusion_matrix.md +0 -0
  47. {rnba-0.2.1 → rnba-0.2.2}/docs/analysis/visualize_core_circuits.py +0 -0
  48. {rnba-0.2.1 → rnba-0.2.2}/docs/bindings-file-format.md +0 -0
  49. {rnba-0.2.1 → rnba-0.2.2}/docs/instruction-file-formats.md +0 -0
  50. {rnba-0.2.1 → rnba-0.2.2}/docs/snb15_population_categories.md +0 -0
  51. {rnba-0.2.1 → rnba-0.2.2}/docs/templates/schematic_template_manifest.md +0 -0
  52. {rnba-0.2.1 → rnba-0.2.2}/examples/Collapsed_network_force.gexf +0 -0
  53. {rnba-0.2.1 → rnba-0.2.2}/examples/Collapsed_network_force_2026-05-22_noring_v1.gexf +0 -0
  54. {rnba-0.2.1 → rnba-0.2.2}/examples/Example_01_WIlsonCowan.ipynb +0 -0
  55. {rnba-0.2.1 → rnba-0.2.2}/examples/Example_02_BasicCircuits.ipynb +0 -0
  56. {rnba-0.2.1 → rnba-0.2.2}/examples/collapse_bgates.py +0 -0
  57. {rnba-0.2.1 → rnba-0.2.2}/examples/colour_gexf.py +0 -0
  58. {rnba-0.2.1 → rnba-0.2.2}/examples/demo_network_visualization.py +0 -0
  59. {rnba-0.2.1 → rnba-0.2.2}/examples/example_2.py +0 -0
  60. {rnba-0.2.1 → rnba-0.2.2}/examples/example_3.py +0 -0
  61. {rnba-0.2.1 → rnba-0.2.2}/examples/example_clamp.py +0 -0
  62. {rnba-0.2.1 → rnba-0.2.2}/examples/example_nCV_generated.py +0 -0
  63. {rnba-0.2.1 → rnba-0.2.2}/examples/example_network_creation.py +0 -0
  64. {rnba-0.2.1 → rnba-0.2.2}/examples/example_qa_create_network.py +0 -0
  65. {rnba-0.2.1 → rnba-0.2.2}/examples/example_qa_generated.py +0 -0
  66. {rnba-0.2.1 → rnba-0.2.2}/examples/example_qa_simulation.py +0 -0
  67. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_circle_16k.gexf +0 -0
  68. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_circle_ei_32k.gexf +0 -0
  69. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_ei_grey_32k.gexf +0 -0
  70. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_ei_grey_seeded_32k.gexf +0 -0
  71. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_force_16k.gexf +0 -0
  72. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_force_ei_32k.gexf +0 -0
  73. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_grey_16k.gexf +0 -0
  74. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_sfdp.gexf +0 -0
  75. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_sfdp_ei.gexf +0 -0
  76. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_sfdp_grey.gephi +0 -0
  77. {rnba-0.2.1 → rnba-0.2.2}/examples/full_network_sfdp_rows12.gexf +0 -0
  78. {rnba-0.2.1 → rnba-0.2.2}/examples/full_scale_sfdp_visualization.py +0 -0
  79. {rnba-0.2.1 → rnba-0.2.2}/examples/gexf_roles.py +0 -0
  80. {rnba-0.2.1 → rnba-0.2.2}/examples/layout_rows.py +0 -0
  81. {rnba-0.2.1 → rnba-0.2.2}/examples/render_collapsed.py +0 -0
  82. {rnba-0.2.1 → rnba-0.2.2}/examples/render_gexf.py +0 -0
  83. {rnba-0.2.1 → rnba-0.2.2}/examples/rnba_roles_row1.gexf +0 -0
  84. {rnba-0.2.1 → rnba-0.2.2}/examples/rnba_roles_snb15.gexf +0 -0
  85. {rnba-0.2.1 → rnba-0.2.2}/examples/row_visualization.py +0 -0
  86. {rnba-0.2.1 → rnba-0.2.2}/examples/seed_circle_ext.py +0 -0
  87. {rnba-0.2.1 → rnba-0.2.2}/examples/seed_ei_gexf.py +0 -0
  88. {rnba-0.2.1 → rnba-0.2.2}/examples/visualize_architecture.sh +0 -0
  89. {rnba-0.2.1 → rnba-0.2.2}/rnba/_sim.py +0 -0
  90. {rnba-0.2.1 → rnba-0.2.2}/rnba/architecture.py +0 -0
  91. {rnba-0.2.1 → rnba-0.2.2}/rnba/blackboard.py +0 -0
  92. {rnba-0.2.1 → rnba-0.2.2}/rnba/circuit.py +0 -0
  93. {rnba-0.2.1 → rnba-0.2.2}/rnba/circuits/__init__.py +0 -0
  94. {rnba-0.2.1 → rnba-0.2.2}/rnba/circuits/core.py +0 -0
  95. {rnba-0.2.1 → rnba-0.2.2}/rnba/cli.py +0 -0
  96. {rnba-0.2.1 → rnba-0.2.2}/rnba/constants.py +0 -0
  97. {rnba-0.2.1 → rnba-0.2.2}/rnba/converters/__init__.py +0 -0
  98. {rnba-0.2.1 → rnba-0.2.2}/rnba/converters/architecture.py +0 -0
  99. {rnba-0.2.1 → rnba-0.2.2}/rnba/inputs.py +0 -0
  100. {rnba-0.2.1 → rnba-0.2.2}/rnba/network.py +0 -0
  101. {rnba-0.2.1 → rnba-0.2.2}/rnba/nodes.py +0 -0
  102. {rnba-0.2.1 → rnba-0.2.2}/rnba/plotting.py +0 -0
  103. {rnba-0.2.1 → rnba-0.2.2}/rnba/schematic_visualization.py +0 -0
  104. {rnba-0.2.1 → rnba-0.2.2}/rnba/simulation.py +0 -0
  105. {rnba-0.2.1 → rnba-0.2.2}/rnba/visualize.py +0 -0
  106. {rnba-0.2.1 → rnba-0.2.2}/tests/QA_3NVN_O_fail.txt +0 -0
  107. {rnba-0.2.1 → rnba-0.2.2}/tests/QA_3NVN_O_success.txt +0 -0
  108. {rnba-0.2.1 → rnba-0.2.2}/tests/RNBA_SGall_SNB15_Bindings_Sentence_3NVN.txt +0 -0
  109. {rnba-0.2.1 → rnba-0.2.2}/tests/RNBA_arch_SGall_SNB15.txt +0 -0
  110. {rnba-0.2.1 → rnba-0.2.2}/tests/RNBA_arch_SGall_SNB20.txt +0 -0
  111. {rnba-0.2.1 → rnba-0.2.2}/tests/RNBA_arch_SGall_SNB30.txt +0 -0
  112. {rnba-0.2.1 → rnba-0.2.2}/tests/Sentence_2CV.txt +0 -0
  113. {rnba-0.2.1 → rnba-0.2.2}/tests/conftest.py +0 -0
  114. {rnba-0.2.1 → rnba-0.2.2}/tests/integration/__init__.py +0 -0
  115. {rnba-0.2.1 → rnba-0.2.2}/tests/integration/test_architecture_vs_gexf.py +0 -0
  116. {rnba-0.2.1 → rnba-0.2.2}/tests/test_architecture.py +0 -0
  117. {rnba-0.2.1 → rnba-0.2.2}/tests/test_basic.py +0 -0
  118. {rnba-0.2.1 → rnba-0.2.2}/tests/test_blackboard.py +0 -0
  119. {rnba-0.2.1 → rnba-0.2.2}/tests/test_circuit.py +0 -0
  120. {rnba-0.2.1 → rnba-0.2.2}/tests/test_circuits.py +0 -0
  121. {rnba-0.2.1 → rnba-0.2.2}/tests/test_clamp_integration.py +0 -0
  122. {rnba-0.2.1 → rnba-0.2.2}/tests/test_cpp_ncv_golden.py +0 -0
  123. {rnba-0.2.1 → rnba-0.2.2}/tests/test_instruction_unified.py +0 -0
  124. {rnba-0.2.1 → rnba-0.2.2}/tests/test_instruction_window_semantics.py +0 -0
  125. {rnba-0.2.1 → rnba-0.2.2}/tests/test_network.py +0 -0
  126. {rnba-0.2.1 → rnba-0.2.2}/tests/test_nodes.py +0 -0
  127. {rnba-0.2.1 → rnba-0.2.2}/tests/test_rnba_arch_converter.py +0 -0
  128. {rnba-0.2.1 → rnba-0.2.2}/tests/test_rnba_simulation_launcher.py +0 -0
  129. {rnba-0.2.1 → rnba-0.2.2}/tests/test_simulation_engines.py +0 -0
  130. {rnba-0.2.1 → rnba-0.2.2}/tests/test_visualize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rnba
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Recurrent Neural Blackboard Architecture - a simulation framework for neuro-cognitive modelling.
5
5
  Author-Email: RNBA Contributors <m.dekamps@leeds.ac.uk>
6
6
  License: MIT
@@ -163,6 +163,8 @@ python -m rnba.visualize_activity_flow \
163
163
 
164
164
  To zoom in on the top of the blackboard, add `--first-rows N` (e.g. `--first-rows 5`): the view is cropped to the first N rows and markers are enlarged for legibility, while the layout and trace mapping stay identical to the full 15-row view. This works for both the single-frame preview (`--preview-time-ms`) and full video rendering, and mirrors the `--first-rows` option of the static schematic renderer (see below).
165
165
 
166
+ Frame rendering dominates the runtime. Pass `--workers N` to render frames across N processes (each holds its own copy of the base schematic); output is identical to a sequential run. `--dpi` trades resolution for size but has little effect on speed, since most per-frame cost is drawing the schematic rather than rasterising pixels.
167
+
166
168
  See `python -m rnba.visualize_activity_flow --help` for the full option set (codec, styling, frame selection).
167
169
 
168
170
  ## Quick Start: Replicating the QA Simulation
@@ -126,6 +126,8 @@ python -m rnba.visualize_activity_flow \
126
126
 
127
127
  To zoom in on the top of the blackboard, add `--first-rows N` (e.g. `--first-rows 5`): the view is cropped to the first N rows and markers are enlarged for legibility, while the layout and trace mapping stay identical to the full 15-row view. This works for both the single-frame preview (`--preview-time-ms`) and full video rendering, and mirrors the `--first-rows` option of the static schematic renderer (see below).
128
128
 
129
+ Frame rendering dominates the runtime. Pass `--workers N` to render frames across N processes (each holds its own copy of the base schematic); output is identical to a sequential run. `--dpi` trades resolution for size but has little effect on speed, since most per-frame cost is drawing the schematic rather than rasterising pixels.
130
+
129
131
  See `python -m rnba.visualize_activity_flow --help` for the full option set (codec, styling, frame selection).
130
132
 
131
133
  ## Quick Start: Replicating the QA Simulation
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "rnba"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Recurrent Neural Blackboard Architecture - a simulation framework for neuro-cognitive modelling."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -3,7 +3,7 @@
3
3
  Simple package with a greet() helper used in tests and CLI.
4
4
  """
5
5
 
6
- __version__ = "0.2.1"
6
+ __version__ = "0.2.2"
7
7
 
8
8
 
9
9
  def greet(name: str = "RNBA") -> str:
@@ -14,11 +14,12 @@ from __future__ import annotations
14
14
  import argparse
15
15
  import csv
16
16
  import io
17
+ import multiprocessing as mp
17
18
  import re
18
19
  import subprocess
19
20
  from dataclasses import dataclass
20
21
  from pathlib import Path
21
- from typing import Dict, Iterable, List, Optional
22
+ from typing import Dict, Iterable, List, Optional, Tuple
22
23
 
23
24
  import matplotlib.pyplot as plt
24
25
  from matplotlib.collections import LineCollection
@@ -1255,24 +1256,33 @@ def write_dynamics_audit_csv(
1255
1256
  )
1256
1257
 
1257
1258
 
1258
- def render_preview(
1259
+ @dataclass
1260
+ class _OverlayContext:
1261
+ """Static rendering state shared by every frame of one video.
1262
+
1263
+ The base schematic figure is expensive to build (thousands of artists);
1264
+ it is created once here, and per-frame overlays are added and removed
1265
+ on top of it.
1266
+ """
1267
+
1268
+ fig: "plt.Figure"
1269
+ ax: "plt.Axes"
1270
+ symbols: List[Symbol]
1271
+ edges: List[SymbolEdge]
1272
+ by_symbol: Dict[str, MappingResult]
1273
+ symbol_pos: Dict[str, Tuple[float, float]]
1274
+ hidden_ids: set
1275
+ overlay_marker_scale: float
1276
+ overlay_line_scale: float
1277
+
1278
+
1279
+ def _build_overlay_context(
1259
1280
  symbols: List[Symbol],
1260
1281
  mappings: List[MappingResult],
1261
1282
  edges: List[SymbolEdge],
1262
- trace_values: Dict[int, float],
1263
- out_png: Path,
1264
- vmax: float,
1265
- base_grey: str,
1266
- base_alpha: float,
1267
- dpi: int,
1268
- base_size: float,
1269
- activation_threshold: float,
1270
- activation_gamma: float,
1271
- active_size_max: float,
1272
- supersample_scale: float,
1273
- first_rows: Optional[int] = None,
1274
- crop_edges: bool = False,
1275
- ) -> None:
1283
+ first_rows: Optional[int],
1284
+ crop_edges: bool,
1285
+ ) -> _OverlayContext:
1276
1286
  by_symbol = {m.symbol_id: m for m in mappings}
1277
1287
  symbol_pos = {s.symbol_id: (s.x, s.y) for s in symbols}
1278
1288
  hidden_families = {"WORD_INHIB"}
@@ -1298,7 +1308,41 @@ def render_preview(
1298
1308
  show_control_nodes=True,
1299
1309
  activity_flow_theme=True,
1300
1310
  )
1301
- ax = fig.axes[0]
1311
+ return _OverlayContext(
1312
+ fig=fig,
1313
+ ax=fig.axes[0],
1314
+ symbols=symbols,
1315
+ edges=edges,
1316
+ by_symbol=by_symbol,
1317
+ symbol_pos=symbol_pos,
1318
+ hidden_ids=hidden_ids,
1319
+ overlay_marker_scale=overlay_marker_scale,
1320
+ overlay_line_scale=overlay_line_scale,
1321
+ )
1322
+
1323
+
1324
+ def _draw_activity_overlay(
1325
+ ctx: _OverlayContext,
1326
+ trace_values: Dict[int, float],
1327
+ vmax: float,
1328
+ activation_threshold: float,
1329
+ activation_gamma: float,
1330
+ active_size_max: float,
1331
+ ) -> list:
1332
+ """Draw one frame's activity overlay onto the base figure.
1333
+
1334
+ Returns the created artists so a video loop can ``remove()`` them
1335
+ before drawing the next frame.
1336
+ """
1337
+ ax = ctx.ax
1338
+ by_symbol = ctx.by_symbol
1339
+ symbol_pos = ctx.symbol_pos
1340
+ hidden_ids = ctx.hidden_ids
1341
+ edges = ctx.edges
1342
+ symbols = ctx.symbols
1343
+ overlay_marker_scale = ctx.overlay_marker_scale
1344
+ overlay_line_scale = ctx.overlay_line_scale
1345
+ artists: list = []
1302
1346
 
1303
1347
  # Source representative activity drives outgoing edge intensity.
1304
1348
  src_activity: Dict[str, float] = {}
@@ -1349,6 +1393,7 @@ def render_preview(
1349
1393
  zorder=5,
1350
1394
  )
1351
1395
  ax.add_collection(active_edge_collection)
1396
+ artists.append(active_edge_collection)
1352
1397
 
1353
1398
  x_act: List[float] = []
1354
1399
  y_act: List[float] = []
@@ -1398,8 +1443,18 @@ def render_preview(
1398
1443
  s_act.append(size)
1399
1444
 
1400
1445
  if x_act:
1401
- ax.scatter(x_act, y_act, s=s_act, c=c_act, edgecolors="none", zorder=6)
1446
+ scatter_artist = ax.scatter(x_act, y_act, s=s_act, c=c_act, edgecolors="none", zorder=6)
1447
+ artists.append(scatter_artist)
1448
+
1449
+ return artists
1402
1450
 
1451
+
1452
+ def _save_frame(
1453
+ fig: "plt.Figure",
1454
+ out_png: Path,
1455
+ dpi: int,
1456
+ supersample_scale: float,
1457
+ ) -> None:
1403
1458
  out_png.parent.mkdir(parents=True, exist_ok=True)
1404
1459
  scale = supersample_scale if supersample_scale > 1.0 else 1.0
1405
1460
  target_w = max(1, int(round(fig.get_figwidth() * dpi)))
@@ -1432,7 +1487,39 @@ def render_preview(
1432
1487
  fig.savefig(out_png, dpi=dpi)
1433
1488
  else:
1434
1489
  fig.savefig(out_png, dpi=super_dpi)
1435
- plt.close(fig)
1490
+
1491
+
1492
+ def render_preview(
1493
+ symbols: List[Symbol],
1494
+ mappings: List[MappingResult],
1495
+ edges: List[SymbolEdge],
1496
+ trace_values: Dict[int, float],
1497
+ out_png: Path,
1498
+ vmax: float,
1499
+ base_grey: str,
1500
+ base_alpha: float,
1501
+ dpi: int,
1502
+ base_size: float,
1503
+ activation_threshold: float,
1504
+ activation_gamma: float,
1505
+ active_size_max: float,
1506
+ supersample_scale: float,
1507
+ first_rows: Optional[int] = None,
1508
+ crop_edges: bool = False,
1509
+ ) -> None:
1510
+ ctx = _build_overlay_context(symbols, mappings, edges, first_rows, crop_edges)
1511
+ try:
1512
+ _draw_activity_overlay(
1513
+ ctx,
1514
+ trace_values,
1515
+ vmax=vmax,
1516
+ activation_threshold=activation_threshold,
1517
+ activation_gamma=activation_gamma,
1518
+ active_size_max=active_size_max,
1519
+ )
1520
+ _save_frame(ctx.fig, out_png, dpi=dpi, supersample_scale=supersample_scale)
1521
+ finally:
1522
+ plt.close(ctx.fig)
1436
1523
 
1437
1524
 
1438
1525
  def _iter_sampled_trace_rows(
@@ -1479,6 +1566,54 @@ def _row_to_values(row: List[str], col_to_idx: Dict[int, int]) -> Dict[int, floa
1479
1566
  return values
1480
1567
 
1481
1568
 
1569
+ # ----------------------------------------------------------------------
1570
+ # Parallel frame rendering
1571
+ # ----------------------------------------------------------------------
1572
+ # Each worker process builds its own base schematic once (the figure is a
1573
+ # live matplotlib object and cannot cross the process boundary), then
1574
+ # renders the frames it is handed. Frames are fully independent, so this
1575
+ # scales close to linearly with the worker count.
1576
+
1577
+ _FRAME_WORKER_STATE: Dict[str, object] = {}
1578
+
1579
+
1580
+ def _frame_worker_init(
1581
+ symbols, mappings, edges, first_rows, crop_edges,
1582
+ frames_dir, dpi, supersample_scale,
1583
+ vmax, activation_threshold, activation_gamma, active_size_max,
1584
+ ) -> None:
1585
+ ctx = _build_overlay_context(symbols, mappings, edges, first_rows, crop_edges)
1586
+ _FRAME_WORKER_STATE.update(
1587
+ ctx=ctx,
1588
+ frames_dir=frames_dir,
1589
+ dpi=dpi,
1590
+ supersample_scale=supersample_scale,
1591
+ vmax=vmax,
1592
+ activation_threshold=activation_threshold,
1593
+ activation_gamma=activation_gamma,
1594
+ active_size_max=active_size_max,
1595
+ )
1596
+
1597
+
1598
+ def _frame_worker_render(task) -> int:
1599
+ idx, values = task
1600
+ st = _FRAME_WORKER_STATE
1601
+ ctx = st["ctx"]
1602
+ overlay_artists = _draw_activity_overlay(
1603
+ ctx,
1604
+ values,
1605
+ vmax=st["vmax"],
1606
+ activation_threshold=st["activation_threshold"],
1607
+ activation_gamma=st["activation_gamma"],
1608
+ active_size_max=st["active_size_max"],
1609
+ )
1610
+ frame_path = st["frames_dir"] / f"frame_{idx:06d}.png"
1611
+ _save_frame(ctx.fig, frame_path, dpi=st["dpi"], supersample_scale=st["supersample_scale"])
1612
+ for artist in overlay_artists:
1613
+ artist.remove()
1614
+ return idx
1615
+
1616
+
1482
1617
  def render_video(
1483
1618
  symbols: List[Symbol],
1484
1619
  mappings: List[MappingResult],
@@ -1510,6 +1645,7 @@ def render_video(
1510
1645
  video_out: Path,
1511
1646
  first_rows: Optional[int] = None,
1512
1647
  crop_edges: bool = False,
1648
+ workers: int = 1,
1513
1649
  ) -> None:
1514
1650
  out_dir.mkdir(parents=True, exist_ok=True)
1515
1651
  frames_dir.mkdir(parents=True, exist_ok=True)
@@ -1522,31 +1658,57 @@ def render_video(
1522
1658
  raise ValueError(f"No sampled rows found in {trace_csv}")
1523
1659
 
1524
1660
  total = len(sampled_rows)
1661
+
1662
+ # Build per-frame activity maps sequentially, up front: the optional
1663
+ # input aligner advances a monotonic cursor, so it must see frames in
1664
+ # time order regardless of how they are later rendered.
1665
+ frame_tasks: List[Tuple[int, Dict[int, float]]] = []
1525
1666
  for idx, (t_ms, row) in enumerate(sampled_rows):
1526
1667
  values = _row_to_values(row, col_to_idx)
1527
1668
  if input_aligner is not None:
1528
1669
  values.update(input_aligner.values_at(t_ms))
1529
- frame_path = frames_dir / f"frame_{idx:06d}.png"
1530
- render_preview(
1531
- symbols=symbols,
1532
- mappings=mappings,
1533
- edges=edges,
1534
- trace_values=values,
1535
- out_png=frame_path,
1536
- vmax=vmax,
1537
- base_grey=base_grey,
1538
- base_alpha=base_alpha,
1539
- dpi=dpi,
1540
- base_size=base_size,
1541
- activation_threshold=activation_threshold,
1542
- activation_gamma=activation_gamma,
1543
- active_size_max=active_size_max,
1544
- supersample_scale=supersample_scale,
1545
- first_rows=first_rows,
1546
- crop_edges=crop_edges,
1670
+ frame_tasks.append((idx, values))
1671
+
1672
+ workers = max(1, int(workers))
1673
+ if workers > 1:
1674
+ workers = min(workers, total)
1675
+ print(f"Rendering {total} frames with {workers} workers ...")
1676
+ init_args = (
1677
+ symbols, mappings, edges, first_rows, crop_edges,
1678
+ frames_dir, dpi, supersample_scale,
1679
+ vmax, activation_threshold, activation_gamma, active_size_max,
1547
1680
  )
1548
- if (idx + 1) % 25 == 0 or (idx + 1) == total:
1549
- print(f"Rendered {idx + 1}/{total} frames")
1681
+ done = 0
1682
+ with mp.Pool(
1683
+ processes=workers,
1684
+ initializer=_frame_worker_init,
1685
+ initargs=init_args,
1686
+ ) as pool:
1687
+ for _ in pool.imap_unordered(_frame_worker_render, frame_tasks, chunksize=4):
1688
+ done += 1
1689
+ if done % 25 == 0 or done == total:
1690
+ print(f"Rendered {done}/{total} frames")
1691
+ else:
1692
+ # Single-process path: reuse one base schematic across all frames.
1693
+ ctx = _build_overlay_context(symbols, mappings, edges, first_rows, crop_edges)
1694
+ try:
1695
+ for idx, values in frame_tasks:
1696
+ overlay_artists = _draw_activity_overlay(
1697
+ ctx,
1698
+ values,
1699
+ vmax=vmax,
1700
+ activation_threshold=activation_threshold,
1701
+ activation_gamma=activation_gamma,
1702
+ active_size_max=active_size_max,
1703
+ )
1704
+ frame_path = frames_dir / f"frame_{idx:06d}.png"
1705
+ _save_frame(ctx.fig, frame_path, dpi=dpi, supersample_scale=supersample_scale)
1706
+ for artist in overlay_artists:
1707
+ artist.remove()
1708
+ if (idx + 1) % 25 == 0 or (idx + 1) == total:
1709
+ print(f"Rendered {idx + 1}/{total} frames")
1710
+ finally:
1711
+ plt.close(ctx.fig)
1550
1712
 
1551
1713
  cmd = [
1552
1714
  "ffmpeg",
@@ -1661,6 +1823,13 @@ def main() -> None:
1661
1823
  parser.add_argument("--fps", type=int, default=25, help="Output video frames per second")
1662
1824
  parser.add_argument("--step-ms", type=float, default=10.0, help="Trace sampling stride in ms for video frames")
1663
1825
  parser.add_argument("--max-frames", type=int, default=0, help="Optional cap on rendered frames (0 means no cap)")
1826
+ parser.add_argument(
1827
+ "--workers",
1828
+ type=int,
1829
+ default=1,
1830
+ help="Parallel frame-rendering worker processes (1 = sequential). "
1831
+ "Each worker holds its own copy of the base schematic.",
1832
+ )
1664
1833
  parser.add_argument(
1665
1834
  "--frames-dir",
1666
1835
  type=Path,
@@ -1774,6 +1943,7 @@ def main() -> None:
1774
1943
  video_out=args.video_out,
1775
1944
  first_rows=args.first_rows,
1776
1945
  crop_edges=args.crop_edges,
1946
+ workers=args.workers,
1777
1947
  )
1778
1948
  print(f"Wrote: {args.video_out}")
1779
1949
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes