Skip to main content

Subsystems

Any .weave file in a project can be run from inside another .weave, the way a Simulink model reference works. There is no separate file type: a file becomes callable by containing Subsystem Argument and Subsystem Return nodes, and its interface (the names and order of its inputs and outputs) is derived from those nodes.

Status. The engine, the three nodes, the canvas (file picker + dynamic ports), and Python export are all in place and covered by tests. Still to come: "extract selection into subsystem".

See projects/demo_project/subsystem/ for a worked example: circle_geometry.weave (a subsystem — one Argument, two Returns) called twice, with different arguments, from subsystem_circle_demo.weave. Every family of shipped demo now lives under a per-family subfolder of projects/demo_project/ (polyweave/, simweave/, sklearn/, torch/, subsystem/, ...).

The three nodes (category Subsystem)​

NodeWhere it goesWhat it does
Subsystem Argument (subsystem_input)in the called fileAn input. Fields: Name, Order, Type (a hint), Default. Emits the value the caller passed for its Name, otherwise its Default.
Subsystem Return (subsystem_output)in the called fileAn output. Fields: Name, Order, Type. Passes its input through and previews it; the caller reads it back by Name.
Run Subsystem (run_subgraph)in the calling fileRuns another .weave of the project. Field: Subsystem file (project-relative path). One input port per Argument, one output port per Return.

Because an Argument emits its Default when the file is run on its own, every subsystem stays runnable and testable standalone.

Names must be valid Python identifiers and unique among a file's Arguments (and among its Returns). args_dict is reserved. Order decides port order (lowest first, ties broken by node id).

How a call behaves​

  • The file on disk runs, not unsaved edits in an open tab.
  • Each call has its own scope: its own dataset, no events on the parent's canvas, the parent's Stop button applies, and its log lines arrive prefixed [sub:<file>].
  • If any node inside fails, the Run Subsystem node fails with the child's node id and message.
  • An Argument left unwired uses its Default.
  • The optional fixed args_dict input takes {argument name: value} pairs and overrides wired arguments of the same name; an unknown name is an error (catches typos). Handy for parameter sweeps and calls inside a loop.
  • Recursion (a calls b calls a) is detected and reported; nesting is capped at 8 levels.
  • The subsystem file must be inside the project (.. escapes are rejected) and end in .weave.
  • An Argument can feed a for loop in the called file: its value is resolved before the loop's iteration count is read.

Ports and the interface snapshot​

Drawflow connections are by port index, so the calling node keeps a snapshot of the port names in its data:

{"weave_file": "sim/quarter_car.weave",
"dynamic_inputs": ["mass", "spring_k"],
"dynamic_outputs": ["displacement", "force"]}

Ports are args_dict, then dynamic_inputs in order; outputs are dynamic_outputs in order. At run time values are matched by name, so a called file that has since changed fails loudly (has no Return named 'x', has no Argument named 'y') instead of silently shifting values between ports. Without a dynamic_outputs snapshot the node follows the file's current Return order.

Python export​

A run_subgraph node's call is inlined the first time it's referenced: PythonGenerator recursively parses the called .weave, exports it as a nested def subsystem_<file>(arg=default, ...): ... return {"name": value, ...}, and turns the call site into node_N = subsystem_<file>(arg=<upstream expr>). Two calls to the same file — even from different subsystems — share one definition; a subsystem that itself calls another is inlined the same way, recursively. node_N["name"] reads a specific Return off the result, matching the runtime's own MultiOutput port selection.

A wired args_dict port isn't representable in exported code (its value isn't known until the script runs), so only that node's export is marked unavailable — same as a missing or unresolvable subsystem file, or an actual recursive call chain (a calls b calls a).

The exported script is still one file — a subsystem doesn't get its own .py. See the "1 large file" discussion in the project's commit history if that changes.

For node authors​

  • ExecutionContext.run_subsystem(path, arguments) runs a child engine and returns {"interface": ..., "returns": {name: value}}.
  • A node with several output ports returns backend.execution.multi_output.MultiOutput({0: a, 1: b}); each downstream connection picks the value for the output port it leaves from. Every other node keeps returning a single value. Declare register_block(..., {"multi_output": True}) so the exporter's PythonGenerator._upstream_ref knows to generate node_N["name"] for a downstream reference (it reads the name from the node's own dynamic_outputs data).
  • register_block(..., {"dynamic_ports": True}) marks a node whose extra input ports are named in its data as dynamic_inputs.
  • register_block(..., {"codegen_custom": "..."}) marks a node whose Python export is bespoke logic in PythonGenerator itself (rather than the generic expr/template mechanism) — codegen_available becomes true without an expr/template present. See PythonGenerator._generate_subsystem_call for the one current user.