bloc.reactors.plug_flow#

Plug-flow reactor (PFR) engines and wall-model subclasses.

Classes#

PFR

Plug Flow Reactor (PFR) engine — closed Lagrangian parcel march.

PFRWallProfile

PFR engine with a prescribed wall-temperature profile.

PFRHomogeneousShell

PFR engine with homogeneous-shell radial heat loss.

PFRThinShell

PFR engine with thin-shell local-temperature radial heat loss.

PFRGasTemperatureProfile

PFR engine with a prescribed GAS temperature profile T_gas(x).

PFRNetwork

Unified network driver for any PFR subclass.

Module Contents#

class bloc.reactors.plug_flow.PFR(gas, *args, **kwargs)#

Bases: bloc.reactors.energy_stream.EnergyPortMixin, cantera.ExtensibleIdealGasConstPressureMoleReactor

Plug Flow Reactor (PFR) engine — closed Lagrangian parcel march.

Physics#

A fixed mass of gas is tracked as it travels axially through a tube. The parcel is modelled as a closed constant-pressure system: no mass enters or leaves during the inner CVODE integration. At every spatial checkpoint the outlet state is written back to the stage reactor via copy_state().

The axial coordinate is mapped to integration time through the local bulk velocity, and the parcel obeys the constant-pressure mole-based balances:

\[u(x) = \frac{\dot m}{\rho(x) S}, \qquad dt = \frac{dx}{u(x)}, \qquad S = \frac{\pi D^2}{4}\]
\[\frac{dn_k}{dt} = V \dot\omega_k, \qquad m\,c_p\,\frac{dT}{dt} = \dot Q_{wall} - V \sum_k \bar h_k \dot\omega_k\]

with \(\bar h_k\) the molar enthalpy and \(\dot\omega_k\) the net molar production rate of species k. The chemical source term is what makes an adiabatic parcel (\(\dot Q_{wall} = 0\)) still change temperature; subclasses differ only in how \(\dot Q_{wall}\) is closed.

The common invariant shared by all PFR subclasses is:

  1. A fresh carrier reactor is built by build_closed_carrier_net() from the stage reactor’s mechanism (_mech_source) and type.

  2. The carrier is marched from inlet to outlet.

  3. The outlet TPX is copied back to the stage reactor.

Subclasses override heat_flux_factory() and/or SOLVER_MODE to express the wall heat-transfer hypothesis:

  • PFRWallProfile — prescribed T_wall(x) with forced-convection + grey-gas radiation, adaptive net.step march.

  • PFRHomogeneousShell — radial insulation stack, spatial-mean temperature, Forward-Backward Sweep.

  • PFRThinShell — radial insulation stack, local T(z), single forward pass.

Why not ct.FlowReactor?#

Cantera’s FlowReactor is a steady-state plug-flow model that is integrated by ReactorNet over axial distance (DAE / IDAS), not over residence time (CVODES). It is adiabatic and frictionless by construction and has no hook for our wall heat-transfer laws. Cantera also enforces that a FlowReactor must run alone in its network — it cannot share a ReactorNet with time-dependent ODE reactors (MFC/PC stages, mixing vessels, recycle loops) because the two formulations use different independent variables and different integrators.

We therefore march a closed Lagrangian parcel on an extensible const-pressure mole reactor, which preserves wall heat injection via after_eval / heat_flux_factory and full compatibility with Boulder’s time-based stage networks.

See also: https://cantera.org/dev/python/zerodim.html#cantera.FlowReactor

param gas:

Cantera Solution at the initial thermodynamic state.

Notes

_meta must be populated by the network or builder before marching. Required keys depend on the subclass heat law; at minimum: length, diameter, mass_flow_rate.

SOLVER_MODE: ClassVar[str] = 'fixed_checkpoint'#
NETWORK_CLASS: ClassVar[type | None] = None#
before_update_state(y)#

Inject a pending initial state into CVODE’s y before the first eval.

heat_flux_factory(carrier)#

Return a heat-flux callable Q(t) [W/m²] for this wall model.

Override in subclasses to provide the wall heat-transfer law. Return None for an adiabatic parcel.

Parameters:

carrier – The fresh closed-carrier instance created by build_closed_carrier_net(). Subclasses may read carrier attributes (diameter, phase, _meta, …) to build the closure.

add_energy_stream(stream)#

Attach an EnergyStream record to this reactor.

property energy_inlets: list[EnergyStream]#

Energy streams currently flowing into this reactor (power_w > 0).

property energy_outlets: list[EnergyStream]#

Energy streams currently flowing out of this reactor (power_w < 0).

class bloc.reactors.plug_flow.PFRWallProfile(gas, *args, clone=False, diameter=0.1, kappa_grey=0.0, mass_flow_rate=0.0, **kwargs)#

Bases: PFR

PFR engine with a prescribed wall-temperature profile.

Physics#

The tube wall temperature T_wall(x) is provided as a callable. At each spatial position the network sets reactor.T_wall_K and reactor.x_position; the after_eval hook then injects the combined forced-convection and grey-gas-radiation flux directly into the energy ODE RHS:

\[Q_{wall} = h_{conv}\,A_{int}\,(T_{wall} - T_{gas}) - q_{rad}\,A_{int}\]

where \(A_{int} = (4/D)\,V\) is the internal surface area and \(h_{conv} = \mathrm{Nu}\,k_{gas}/D\) is computed from the Nusselt correlation (internal_convection_h(), Baehr-Stephan / Hausen / Churchill-Zajic depending on the flow regime).

The radiative flux uses the grey-gas approximation with a cylindrical mean-beam-length integral (compute_gas2wall_radiative_flux()) — not an optically-thin volumetric law:

\[q_{rad} = \left(\frac{2}{3} - f_{trans}\right) \sigma \left(T_{gas}^4 - T_{wall}^4\right), \qquad f_{trans} = 2 \int_0^{\pi/2} e^{-\kappa D \cos\theta} \cos^2\theta \sin\theta \, d\theta\]

with \(\kappa\) = kappa_grey; \(\kappa = 0\) disables gas radiation.

The parcel is marched with an adaptive net.step() loop so the integrator controls step size according to local stiffness.

Use cases#

  • Electrically or externally heated tube furnace with a known wall temperature profile.

  • Any reactor where the wall temperature is prescribed rather than computed from an insulation model.

See also

PFRHomogeneousShell

thick-shell radial loss.

PFRThinShell

thin-shell local-T radial loss.

SOLVER_MODE: ClassVar[str] = 'adaptive'#
diameter = 0.1#
kappa_grey = 0.0#
mass_flow_rate = 0.0#
T_wall_K: float | None = None#
x_position: float = 0.0#
wall_heat_terms()#

Return (Q_conv, Q_rad, h_conv, kappa) for the current phase state.

after_eval is an ODE right-hand-side hook, so the solver also calls it for trial steps it later rejects and for every finite-difference Jacobian column. _last_Q_conv therefore holds whatever perturbed state was evaluated last, which is not the accepted solution and is not reproducible across machines. Anything reporting these terms must recompute them from the accepted state by calling this.

after_eval(t, LHS, RHS)#

Inject wall convection + radiation into the energy ODE RHS.

NETWORK_CLASS: ClassVar[type | None] = None#
before_update_state(y)#

Inject a pending initial state into CVODE’s y before the first eval.

heat_flux_factory(carrier)#

Return a heat-flux callable Q(t) [W/m²] for this wall model.

Override in subclasses to provide the wall heat-transfer law. Return None for an adiabatic parcel.

Parameters:

carrier – The fresh closed-carrier instance created by build_closed_carrier_net(). Subclasses may read carrier attributes (diameter, phase, _meta, …) to build the closure.

add_energy_stream(stream)#

Attach an EnergyStream record to this reactor.

property energy_inlets: list[EnergyStream]#

Energy streams currently flowing into this reactor (power_w > 0).

property energy_outlets: list[EnergyStream]#

Energy streams currently flowing out of this reactor (power_w < 0).

class bloc.reactors.plug_flow.PFRHomogeneousShell(gas, *args, **kwargs)#

Bases: PFR

PFR engine with homogeneous-shell radial heat loss.

Physics#

Axial wall conduction is assumed to smear out gradients, so the wall is isothermal and radial heat loss is driven by the spatial-mean gas temperature through a lumped resistance stack (insulation layers + external natural convection and radiation):

\[\bar T_g = \frac{1}{L}\int_0^L T_g(x)\,dx, \qquad \Phi = f_{corr}\,\frac{\bar T_g - T_{amb}}{R_{tot}}\]
\[R_{tot} = \sum_i \frac{\ln(r_{i+1}/r_i)}{2\pi k_i L} + \frac{1}{h_{eff} S_{ext}}, \qquad h_{eff} = h_{CC} + h_{rad}, \qquad h_{rad} = 4 \varepsilon \sigma T_m^3\]

where \(f_{corr}\) is heat_loss_corr_factor (thermal bridges and other unmodelled paths). See thermal_resistance_stack() and bloc.chem.heat_transfer.compute_heat_losses_linear().

The heat loss enters the Cantera ODE via a ct.Wall (area = 1 m²) whose heat_flux callable distributes Phi_kW uniformly in space (i.e. proportional to local velocity):

\[q_{wall}(t) = -\Phi \frac{m\,u(t)}{\dot m L} \quad [W/m^2]\]

Since \(\bar T_g\) depends on \(\Phi\) and vice versa, the total loss is converged by a Forward-Backward Sweep (FBS): an adiabatic predictor pass gives the mean T, which yields a first Phi_kW estimate; the corrector repeats with the updated wall flux until |ΔΦ| / Φ < 1 %.

Use cases#

  • Insulated, thick-shell industrial vessels where axial wall conduction smears temperature gradients (e.g. the SPRING Carbon Growth Reactor).

  • Any reactor where the dominant resistance to heat loss is in the insulation layer rather than the gas film.

Not intended for#

  • Reactors with large axial temperature gradients and thin walls: use PFRThinShell instead.

See also

PFRWallProfile

prescribed wall-temperature profile.

PFRThinShell

thin-shell local-T radial loss.

SOLVER_MODE: ClassVar[str] = 'fixed_checkpoint'#
thermal_resistance_stack()#

Build the radial-conduction + external-convection resistance list.

Uses _meta geometry and insulation layers. Returns a list of thermal resistances [K/W] from inner wall to ambient, ordered from innermost insulation layer to external convection/radiation.

wall_thermal_report(T_mean_C)#

Return the wall heat-transfer breakdown at a mean gas temperature.

Single source of truth for the SPRING wall KPIs: the external film coefficients (h_CC, h_rad, h_eff [W/m²/K]), the external wall temperature (T_wall_ext [°C]) and the resulting wall heat loss (heat_loss_kW, including heat_loss_corr_factor). When _meta["adiabatic"] is set the loss is zero and the wall sits at ambient.

Parameters:

T_mean_C – Spatial-mean gas temperature in °C.

Returns:

{"h_CC", "h_rad", "h_eff", "T_wall_ext", "heat_loss_kW"}.

Return type:

dict

spatial_mean_temperature(states)#

Compute the spatial-mean gas temperature [°C] from a PFR profile.

Integrates T over the axial position recorded by _march_pfr_to_length in the x extra column, using the trapezoid rule (trapezoid(states.T, states.x) / states.x[-1]) so the FBS heat-loss estimate is identical between the staged and standalone paths.

Parameters:

states – SolutionArray with an x extra column (axial position [m]).

Returns:

Length-weighted mean temperature in °C. Falls back to the unweighted mean only if states.x is missing or zero.

Return type:

float

heat_loss(T_mean_C)#

Compute total wall heat loss [kW] for a given mean gas temperature.

Uses the linearized thermal resistance stack from thermal_resistance_stack() and multiplies by _meta["heat_loss_corr_factor"] (default 1.0) to account for thermal bridges and other unmodelled loss paths. Returns 0 when _meta["adiabatic"] is True.

Parameters:

T_mean_C – Spatial-mean gas temperature in °C.

Returns:

Heat loss in kW (positive = loss to ambient).

Return type:

float

solve_forward(Phi_kW)#

Integrate the PFR with a fixed heat-loss power and return the spatial profile.

Builds a PFRHomogeneousShellNet and delegates to its _spatial_ode_pass. This is the single-shot entry point for standalone use and tests; the FBS convergence loop in PFRHomogeneousShellNet.advance calls _spatial_ode_pass directly.

Parameters:

Phi_kW – Total wall heat loss to subtract from the inlet enthalpy [kW]. Positive value = heat removed from the gas.

Returns:

Spatial profile with extra columns t (residence time [s]) and x (axial position [m]).

Return type:

ct.SolutionArray

NETWORK_CLASS: ClassVar[type | None] = None#
before_update_state(y)#

Inject a pending initial state into CVODE’s y before the first eval.

heat_flux_factory(carrier)#

Return a heat-flux callable Q(t) [W/m²] for this wall model.

Override in subclasses to provide the wall heat-transfer law. Return None for an adiabatic parcel.

Parameters:

carrier – The fresh closed-carrier instance created by build_closed_carrier_net(). Subclasses may read carrier attributes (diameter, phase, _meta, …) to build the closure.

add_energy_stream(stream)#

Attach an EnergyStream record to this reactor.

property energy_inlets: list[EnergyStream]#

Energy streams currently flowing into this reactor (power_w > 0).

property energy_outlets: list[EnergyStream]#

Energy streams currently flowing out of this reactor (power_w < 0).

class bloc.reactors.plug_flow.PFRThinShell(gas, *args, **kwargs)#

Bases: PFR

PFR engine with thin-shell local-temperature radial heat loss.

Physics#

Heat loss depends on the local gas temperature T(z) rather than the spatial mean. The local loss per unit length is:

\[q'(z) = -\frac{T_{gas}(z) - T_{amb}}{R_{tot} \cdot L}\quad [W/m]\]

Because the flux is evaluated at each checkpoint via a callable that reads reactor.phase.T, a single forward pass is sufficient — no Forward-Backward Sweep is needed.

Use cases#

  • Quartz-tube / lab-tube reactors where axial wall conduction is negligible (thin, low-conductivity walls).

  • Scenarios where the gas temperature profile is steep and a spatial-mean-T hypothesis would over-predict the loss in the hot inlet zone.

See also

PFRWallProfile

prescribed wall-temperature profile.

PFRHomogeneousShell

thick-shell homogeneous loss.

SOLVER_MODE: ClassVar[str] = 'fixed_checkpoint'#
thermal_resistance_stack#
wall_thermal_report#
spatial_mean_temperature#
heat_loss#
solve_forward()#

Integrate the PFR with the local-T heat loss and return the spatial profile.

Single-shot entry point for standalone use and tests. Builds a PFRThinShellNet and runs one advance pass.

Returns:

Spatial profile with extra columns t and x.

Return type:

ct.SolutionArray

NETWORK_CLASS: ClassVar[type | None] = None#
before_update_state(y)#

Inject a pending initial state into CVODE’s y before the first eval.

heat_flux_factory(carrier)#

Return a heat-flux callable Q(t) [W/m²] for this wall model.

Override in subclasses to provide the wall heat-transfer law. Return None for an adiabatic parcel.

Parameters:

carrier – The fresh closed-carrier instance created by build_closed_carrier_net(). Subclasses may read carrier attributes (diameter, phase, _meta, …) to build the closure.

add_energy_stream(stream)#

Attach an EnergyStream record to this reactor.

property energy_inlets: list[EnergyStream]#

Energy streams currently flowing into this reactor (power_w > 0).

property energy_outlets: list[EnergyStream]#

Energy streams currently flowing out of this reactor (power_w < 0).

class bloc.reactors.plug_flow.PFRGasTemperatureProfile(gas, *args, clone=False, diameter=0.1, mass_flow_rate=0.0, **kwargs)#

Bases: PFR

PFR engine with a prescribed GAS temperature profile T_gas(x).

Physics#

This is the Dirichlet-on-gas boundary condition: the gas temperature itself is prescribed as a callable T_gas(x) and the energy equation is switched off. Only the kinetics evolve; at every spatial checkpoint the network resets the parcel temperature to T_gas(x):

\[T_{gas}(x) = T_{prescribed}(x), \qquad \frac{dY_k}{dt} = \frac{\dot\omega_k W_k}{\rho}\]

No wall film, no radiation and no resistance stack are involved — the temperature is imposed, not computed from a heat balance. Contrast with PFRWallProfile, which prescribes the wall temperature and lets the gas lag behind it through a convective + radiative film.

Numerically the parcel is marched by the shared march_lagrangian_parcel() with energy_off=True; the temperature is re-imposed before each checkpoint advance so the stepwise-constant history converges to the continuous profile as n_points grows. This is the closed-parcel / OO counterpart of the procedural solve_fixed_position_temperature_profile_pfr().

Use cases#

  • Kinetics validation against a measured in-stream gas temperature (thermocouple trace) where you want predicted species, not a predicted temperature.

  • Decoupling chemistry from the energy balance to isolate mechanism behaviour under a known thermal history.

See also

PFRWallProfile

prescribed wall temperature with convective + radiative coupling (gas temperature is free).

solve_fixed_position_temperature_profile_pfr()

procedural equivalent.

SOLVER_MODE: ClassVar[str] = 'fixed_checkpoint'#
IMPOSED_GAS_T: ClassVar[bool] = True#
diameter = 0.1#
mass_flow_rate = 0.0#
T_gas_K: float | None = None#
x_position: float = 0.0#
NETWORK_CLASS: ClassVar[type | None] = None#
before_update_state(y)#

Inject a pending initial state into CVODE’s y before the first eval.

heat_flux_factory(carrier)#

Return a heat-flux callable Q(t) [W/m²] for this wall model.

Override in subclasses to provide the wall heat-transfer law. Return None for an adiabatic parcel.

Parameters:

carrier – The fresh closed-carrier instance created by build_closed_carrier_net(). Subclasses may read carrier attributes (diameter, phase, _meta, …) to build the closure.

add_energy_stream(stream)#

Attach an EnergyStream record to this reactor.

property energy_inlets: list[EnergyStream]#

Energy streams currently flowing into this reactor (power_w > 0).

property energy_outlets: list[EnergyStream]#

Energy streams currently flowing out of this reactor (power_w < 0).

class bloc.reactors.plug_flow.PFRNetwork(reactor)#

Unified network driver for any PFR subclass.

A generic single-pass driver that runs the fixed-checkpoint _march_pfr_to_length() spatial march for any PFR subclass. Specialised stage networks (PFRWallProfileNet for the tube furnace, PFRHomogeneousShellNet / PFRThinShellNet for the shell models) wrap the same marcher with their wall-loss specifics; this class is the minimal fallback driver.

The network is Boulder-compatible: it exposes time and an advance() method, and wires into the NETWORK_CLASS attribute on each PFR subclass.

Parameters:

reactor – A PFR (or subclass) instance that has been fully configured (_meta populated by the STONE builder).

reactor#
property time: float#

Residence time [s] after the last advance.

advance(time=0.0)#

Run the spatial march and update the stage reactor outlet state.

The time argument is ignored (Boulder calls advance(1.0) by convention); the actual integration length is taken from reactor._meta['length'].