Minimal runnable RefractoryReactor / PFRHomogeneousShellNet example.

When to use this script#

This file is the standalone Python example: it constructs a RefractoryReactor directly, attaches the ambient Reservoir and _loss_wall by hand, then calls RefractoryReactor.solve_forward() and PFRHomogeneousShellNet.advance(). No YAML, no Boulder normalisation, no plugin unfolder — useful for unit testing, debugging, or when you need fine-grained control over the reactor object.

If you instead want the production STONE / Boulder pipeline (where the ambient reservoir and the radial loss wall are produced automatically by the unfolder), use examples/design_pfr_example.yaml and the bloc CLI; see that file’s header for the procedure.

How to run this script#

From the repository root:

conda run -n bloc python examples/run_design_pfr_example.py

The script prints two summaries: the direct solve_forward pass and the FBS-converged pass through PFRHomogeneousShellNet.advance.

What RefractoryReactor does#

RefractoryReactor is a length-marched plug-flow reactor with a homogeneous-shell radial heat-loss model. The chemistry is integrated along the tube axis at constant pressure (IdealGasConstPressureMoleReactor family); the wall heat loss is modelled as a single Cantera Wall between the reactor and an ambient Reservoir.

Two entry points exist:

  • RefractoryReactor.solve_forward(Phi_kW) integrates the tube once with a user-imposed total wall loss Phi_kW distributed uniformly along the length. Useful as a single-shot sanity check.

  • PFRHomogeneousShellNet.advance(time) runs a Forward-Backward Sweep (FBS) loop: it iterates solve_forward while updating Phi_kW from the linear resistance stack evaluated at the spatial-mean gas temperature, until the loss estimate converges (CONVERGENCE_TOL = 1 % or MAX_ITER passes).

Required _meta parameters#

_meta carries the geometry and thermal hypotheses that RefractoryReactor.thermal_resistance_stack and RefractoryReactor.heat_loss consume:

  • mechanism – Cantera mechanism file used by the gas phase.

  • length [m] – tube length; sets reactor.volume (with diameter) and the integration domain.

  • diameter [m] – inner tube diameter; sets the cross-section used to reconstruct axial velocity from mass_flow_rate.

  • mass_flow_rate [kg/s] – process feed flow.

  • eps_wall – external wall emissivity (linearised radiation).

  • T_wall_hyp [degC] – assumed external wall temperature, used to evaluate natural-convection and radiation coefficients.

  • T_amb [degC] – ambient temperature on the outside of the insulation.

  • insulation – dict with e_insul (layer thicknesses [m]) and conductivity (layer thermal conductivities [W/m/K]); ordered inner-to-outer. The radial resistance stack is built from these layers plus an outer natural-convection + linearised-radiation resistance.

  • adiabatic (optional) – if True, all wall losses are skipped and no _loss_wall is needed.

  • heat_loss_corr_factor (optional, default 1.0) – multiplier on the computed Phi to account for thermal bridges and other unmodelled loss paths.

In the STONE / Boulder workflow these fields are read from the YAML and _meta plus the ambient Reservoir and _loss_wall are produced automatically by _build_pfr_homogeneous_shell and the _unfold_design_pfr_loss unfolder. This standalone example performs the same setup by hand.

import math

import cantera as ct

from bloc.reactor_models import PFRHomogeneousShellNet, RefractoryReactor
from bloc.utils import get_mechanism_path


def main() -> None:
    """Run one direct forward solve and one FBS solve."""
    mechanism = get_mechanism_path("Fincke_GRC.yaml")

    gas = ct.Solution(mechanism)
    gas.TPX = 1800.0, 1e5, "CH4:0.5,H2:0.5"

    reactor = RefractoryReactor(gas, clone=False)
    reactor._meta = {
        "design_type": "RefractoryReactor",
        "mechanism": mechanism,
        "length": 1.0,  # [m]    tube length (axial integration domain)
        "diameter": 0.1,  # [m]    inner tube diameter
        "mass_flow_rate": 1e-3,  # [kg/s] process feed
        "eps_wall": 0.9,  #        external wall emissivity
        "T_wall_hyp": 100.0,  # [degC] external wall temperature hypothesis
        "T_amb": 25.0,  # [degC] ambient temperature
        "insulation": {
            "e_insul": {"layer1": 0.05},  # [m]      layer thicknesses (inner -> outer)
            "conductivity": {"layer1": 0.1},  # [W/m/K]  layer thermal conductivities
        },
    }
    reactor.volume = (
        math.pi * reactor._meta["diameter"] ** 2 * reactor._meta["length"] / 4
    )

    # Standalone replacement for the STONE unfolder's ambient + loss wall:
    # PFRHomogeneousShellNet._spatial_ode_pass requires reactor._loss_wall when not adiabatic.
    ambient_gas = ct.Solution(mechanism)
    ambient_gas.TPX = float(reactor._meta["T_amb"]) + 273.15, 101325.0, "N2:1"
    ambient = ct.Reservoir(ambient_gas, name="pfr_ambient")
    reactor._ambient_reservoir = ambient
    reactor._loss_wall = ct.Wall(ambient, reactor, A=1.0, name="pfr_loss_wall")

    states_forward = reactor.solve_forward(Phi_kW=5.0)
    print("Direct solve_forward")
    print(f"  points: {len(states_forward)}")
    print(f"  T_in [K]:  {float(states_forward.T[0]):.6f}")
    print(f"  T_out [K]: {float(states_forward.T[-1]):.6f}")

    net = PFRHomogeneousShellNet([reactor], meta=dict(reactor._meta))
    net.advance(time=1.0)
    states_fbs = reactor._states

    print("FBS via PFRHomogeneousShellNet.advance")
    print(f"  heat_loss [kW]: {float(reactor._heat_loss_kW):.6f}")
    print(f"  points: {len(states_fbs)}")
    print(f"  T_out [K]: {float(states_fbs.T[-1]):.6f}")


if __name__ == "__main__":
    main()