Mixing two streams example — Boulder conversion.

Boulder context#

This example demonstrates how to use Boulder’s sim2stone functionality to convert a Cantera reactor network simulation to a STONE serialised .yaml format, and how to reload and inspect it with BoulderRunner.

The Cantera simulation setup is described in mix1.py, which implements the mixing example from https://cantera.org/3.1/examples/python/reactors/mix1.html.

The corresponding mix1.yaml STONE file is already present in the examples/ directory. In practice you generate it once with:

sim2stone mix1.py

or by calling write_sim_as_yaml() directly, then commit the resulting YAML to your repository. Subsequent runs (CI, docs) load from the committed file so that the full Cantera solve is not repeated on every build.

Note

This is the script shape generated by the Boulder GUI.

When you load a YAML in the Boulder interface and click Download Python, the downloaded .py file follows the same solve_stage() pattern used here. For a multi-stage (PSR → PFR) example see Report converged reactor states.

Requires: cantera, boulder

from pathlib import Path

from boulder.runner import BoulderRunner

# sphinx-gallery sets cwd to the examples directory before executing each
# script, so Path.cwd() is the reliable way to locate sibling files.
yaml_path = str(Path.cwd() / "mix1.yaml")

Load the STONE YAML produced by sim2stone#

from_yaml runs load → normalise → validate without executing any Cantera code. The YAML was generated once from mix1.py via:

from boulder.sim2stone import write_sim_as_yaml
import mix1
write_sim_as_yaml(mix1.sim, "mix1.yaml")

and is committed to the repository so the docs build does not need to re-run the Cantera simulation.

runner = BoulderRunner.from_yaml(yaml_path)
print(f"Loaded STONE YAML: {yaml_path}")
Loaded STONE YAML: /home/runner/work/boulder/boulder/examples/mix1.yaml

Inspect the network topology (no Cantera required)#

build_stage_graph is pure config parsing; no Cantera objects are created. It returns the topologically-sorted execution plan.

plan = runner.build_stage_graph()
print(f"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}"
    )
Execution plan: 1 stage(s)
  Stage 1: 'default'  nodes=['Air Reservoir', 'Fuel Reservoir', 'Mixer', 'Outlet Reservoir']  mechanism=gri30.yaml

Inspect nodes and connections from the config#

nodes = runner.config.get("nodes", [])
connections = runner.config.get("connections", [])
print(f"\nNetwork has {len(nodes)} nodes and {len(connections)} connections.")
for node in nodes:
    nid = node["id"]
    reactor_type = node.get("type", "?")
    print(f"  {nid!r:25s}  type={reactor_type}")
Network has 4 nodes and 3 connections.
  'Air Reservoir'            type=Reservoir
  'Fuel Reservoir'           type=Reservoir
  'Mixer'                    type=IdealGasReactor
  'Outlet Reservoir'         type=Reservoir

Verify the YAML structure matches expectations#

expected_node_ids = {"Air Reservoir", "Fuel Reservoir", "Mixer", "Outlet Reservoir"}
actual_node_ids = {n["id"] for n in nodes}
assert actual_node_ids == expected_node_ids, f"Unexpected nodes: {actual_node_ids}"
assert len(connections) == 3
print("\nYAML structure verified — all assertions passed!")
YAML structure verified — all assertions passed!

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