—
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 lossPhi_kWdistributed uniformly along the length. Useful as a single-shot sanity check.PFRHomogeneousShellNet.advance(time)runs a Forward-Backward Sweep (FBS) loop: it iteratessolve_forwardwhile updatingPhi_kWfrom the linear resistance stack evaluated at the spatial-mean gas temperature, until the loss estimate converges (CONVERGENCE_TOL = 1 %orMAX_ITERpasses).
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; setsreactor.volume(with diameter) and the integration domain.diameter[m] – inner tube diameter; sets the cross-section used to reconstruct axial velocity frommass_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 withe_insul(layer thicknesses [m]) andconductivity(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_wallis needed.heat_loss_corr_factor(optional, default 1.0) – multiplier on the computedPhito 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()