Staged solve with progressive network visualization.


Demonstrates how to call build_viz_network() after each solve_stage() to visualize the reactor network as it grows stage by stage.

This is the detailed companion to the simple Report converged reactor states example. That example shows the minimal script generated by Download Python in the Boulder GUI; this example shows how to extend it to inspect intermediate results at each stage boundary.

Note

The Download Python script follows this same call structure.

The .py file produced by the Boulder GUI’s Download Python button is identical to plot_staged_solve_from_yaml.py. You can extend it with the per-stage draw and inspection calls shown here without changing any of the core solve logic.

Workflow#

  1. from_yaml — load, normalize, validate the YAML config

  2. build_stage_graph — parse topology (pure Python, no Cantera)

  3. For each stage:

    1. solve_stage — build + integrate reactors to steady state

    2. build_viz_network — assemble a ReactorNet containing only the stages solved so far

    3. network.draw — visualise the partial network

  4. Final build_viz_network — full network with all connections restored

Requires: cantera, boulder, graphviz (optional, for network.draw)

from pathlib import Path
from typing import Any

from boulder.runner import BoulderRunner

Load and parse the configuration#

from_yaml runs load → normalize → validate. build_stage_graph is pure config parsing; no Cantera code runs yet.

# sphinx-gallery sets cwd to the examples directory; configs/ is one level up.
config_path = str(Path.cwd().parent / "configs" / "staged_psr_pfr.yaml")
runner = BoulderRunner.from_yaml(config_path)
plan = runner.build_stage_graph()

print(f"Staged execution plan: {len(plan.ordered_stages)} stage(s)")
for i, stage in enumerate(plan.ordered_stages):
    print(
        f"  Stage {i + 1}: '{stage.id}'  nodes={stage.node_ids}"
        f"  mechanism={stage.mechanism}"
    )
Staged execution plan: 2 stage(s)
  Stage 1: 'psr_stage'  nodes=['feed', 'psr']  mechanism=gri30.yaml
  Stage 2: 'pfr_stage'  nodes=['pfr_cell_1', 'pfr_cell_2', 'pfr_cell_3', 'pfr_cell_4']  mechanism=gri30.yaml

Initialise trajectory and inter-stage state carrier#

trajectory = runner.new_trajectory()
inlet_states: dict[str, Any] = {}

Stage 1/2: psr_stage — perfectly-stirred reactor#

solve_stage creates the Cantera reactor objects for this stage, builds an intra-stage ct.ReactorNet, and advances to steady state. The converged outlet state is written into inlet_states so that the next stage can use it as its initial condition.

stage_1 = plan.ordered_stages[0]
runner.solve_stage(plan, stage_1, inlet_states, trajectory)
print(f"\n[{stage_1.id}] solved")
assert runner.converter is not None
for nid in stage_1.node_ids:
    r = runner.converter.reactors[nid]
    print(f"  {nid}: T = {r.phase.T:.1f} K  P = {r.phase.P / 1e5:.3f} bar")
[psr_stage] solved
  feed: T = 300.0 K  P = 1.013 bar
  psr: T = 2105.1 K  P = 1.013 bar

Visualize the network after stage 1#

build_viz_network assembles a single ReactorNet from all reactor objects built so far. After stage 1 it contains only the PSR.

runner.build_viz_network(plan, trajectory)
net_after_stage1 = runner.network
assert net_after_stage1 is not None
print(f"\nViz network after stage 1: {[r.name for r in net_after_stage1.reactors]}")

try:
    diagram = net_after_stage1.draw(print_state=True, species="X")
    diagram.render("network_after_stage1", format="png", cleanup=True)
    print("Saved network_after_stage1.png")
except ImportError:
    print("graphviz not installed — skipping draw()")
Viz network after stage 1: ['psr']
Saved network_after_stage1.png

Stage 2/2: pfr_stage — plug-flow reactor chain#

The PSR outlet state is already in inlet_states; solve_stage uses it to initialise the first PFR cell.

stage_2 = plan.ordered_stages[1]
runner.solve_stage(plan, stage_2, inlet_states, trajectory)
print(f"\n[{stage_2.id}] solved")
assert runner.converter is not None
for nid in stage_2.node_ids:
    r = runner.converter.reactors[nid]
    print(f"  {nid}: T = {r.phase.T:.1f} K  P = {r.phase.P / 1e5:.3f} bar")
[pfr_stage] solved
  pfr_cell_1: T = 2117.9 K  P = 1.013 bar
  pfr_cell_2: T = 2176.6 K  P = 1.013 bar
  pfr_cell_3: T = 2194.5 K  P = 1.013 bar
  pfr_cell_4: T = 2197.0 K  P = 1.013 bar

Visualize the complete network#

Now build_viz_network connects all stages, restoring inter-stage connections (MassFlowControllers etc.) in the full ct.ReactorNet.

runner.build_viz_network(plan, trajectory)
network = runner.network
assert network is not None
print(f"\nFull viz network: {[r.name for r in network.reactors]}")

try:
    diagram = network.draw(print_state=True, species="X")
    diagram.render("network_complete", format="png", cleanup=True)
    print("Saved network_complete.png")
except ImportError:
    print("graphviz not installed — skipping draw()")
Full viz network: ['psr', 'pfr_cell_1', 'pfr_cell_2', 'pfr_cell_3', 'pfr_cell_4']
Saved network_complete.png

Report final reactor states#

print("\nSimulation completed.")
print(f"{'Reactor':<20} {'T [K]':>10} {'P [bar]':>10}")
print("-" * 42)
for r in network.reactors:
    print(f"{r.name:<20} {r.phase.T:>10.1f} {r.phase.P / 1e5:>10.4f}")
Simulation completed.
Reactor                   T [K]    P [bar]
------------------------------------------
psr                      2105.1     1.0132
pfr_cell_1               2117.9     1.0133
pfr_cell_2               2176.6     1.0132
pfr_cell_3               2194.5     1.0133
pfr_cell_4               2197.0     1.0133

Total running time of the script: (0 minutes 0.428 seconds)