--- file_format: mystnb kernelspec: name: python3 --- ```{py:currentmodule} cantera ``` # Extending Reactor Models with Custom Equations In some cases, Cantera's built-in reactor types are insufficient to model a problem. In this situation, the {py:class}`ExtensibleReactor` family of classes can be used to modify and governing equations, starting from one of the built-in reactor types. These reactor types allow the methods that implement the governing equations and related reactor configuration to be augmented or replaced with user-defined Python functions. New state variables can be added to the reactor, and existing ones can be redefined. User-defined `ExtensibleReactor` implementations can be used alongside the built-in reactor types to build reactor networks that can be integrated using the {py:class}`ReactorNet` integrator provided by Cantera. This guide introduces the use of the `ExtensibleReactor` model to modify the governing equations for a reactor. ## Modifying Existing Governing Equations The variables in the governing equations for a reactor that are differentiated with respect to time are known as the *state variables*. The state variables depend on the type of reactor base class chosen. For example, choosing an [ideal gas constant pressure reactor](/reference/reactors/ideal-gas-constant-pressure-reactor) allows the user to modify the governing equations for the following state variables: - $m$, the mass of the reactor's contents (in kg) - $T$, the temperature (in K) - $Y_k$, the mass fractions for each species (dimensionless) As shown in the derivations of the governing equations, the equations implemented by the {ct}`IdealGasConstPressureReactor` class are: $$ \frac{dm}{dt} = \sum_\t{in} \dot{m}_\t{in} - \sum_\t{out} \dot{m}_\t{out} + \dot{m}_\t{wall} $$ $$ m c_p \frac{dT}{dt} = \dot{Q} - \sum_k h_k \dot{m}_{k,\t{gen}} + \sum_\t{in} \dot{m}_\t{in} \left(h_\t{in} - \sum_k h_k Y_{k,\t{in}} \right) $$ $$ \frac{d(mY_k)}{dt} = \sum_\t{in} \dot{m}_\t{in} Y_{k,\t{in}} - \sum_\t{out} \dot{m}_\t{out} Y_k + \dot{m}_{k,gen} $$ Each of these equations is written with an expression on the left-hand side (LHS) multiplied by the time derivative of a state variable which equals an expression on the right-hand side (RHS). The interface of the reactor network model is defined so that the reactor governing equations are evaluated by calculating these LHS and RHS expressions for each governing equation. The extensible reactor model then allows calculation of these LHS and RHS terms to be replaced or augmented by user-provided methods. For example, to add a term for a large mass, say a rock, inside the reactor that affects the thermal mass, the energy equation would become: $$ \left(m c_p + m_\t{rock} c_{p,\t{rock}}\right) \frac{dT}{dt} = \dot{Q} - \sum_k h_k \dot{m}_{k,\t{gen}} + \sum_\t{in} \dot{m}_\t{in} \left(h_\t{in} - \sum_k h_k Y_{k,\t{in}} \right) $$ Here, the LHS coefficient has changed from $m c_p$ to $m c_p + m_{\t{rock}} c_{p,\t{rock}}$. Since the rock does not change the composition of the species in the reactor and does not change the mass flow rate of any inlets or outlets, the other governing equations defining the ideal gas constant pressure reactor can be left unmodified. To implement this change, we define a new class derived from {py:class}`ExtensibleIdealGasConstPressureReactor`. In this new class, we then define an implementation for the `after_eval` method which has the signature ```py def after_eval(self, t, LHS, RHS) -> None ``` where `t` is the current simulation time and `LHS` and `RHS` are arrays with one element for each state variable. In this case, the length of each array is two plus the number of species. Optional user-defined methods for the `ExtensibleReactor` class start with the prefix `before_`, `after_`, or `replace_`, indicating whether the this method will called before, after, or instead of the corresponding method from the base class. In this case, our method will be called after the `eval` method of {ct}`IdealGasConstPressureReactor`, with the values of `LHS` and `RHS` already set based on the original governing equations. Instead of `after_eval`, we could implement either `before_eval` or `replace_eval` methods. However, in the case of `before_eval`, any element of `LHS` that we set would be overwritten by the initial implementation, and if we use `replace_eval`, we would have needed to calculate values for all elements of `RHS` and `LHS` instead of just modifying a single value. To obtain the above modification to the governing equations, the implementation of `after_eval` inside a new reactor class is then: ```{code-cell} python import cantera as ct class RockReactor(ct.ExtensibleIdealGasConstPressureReactor): def __init__(self, *args, mass_rock, cp_rock, **kwargs): super().__init__(*args, **kwargs) self.mass_rock = mass_rock self.cp_rock = cp_rock def after_eval(self, t, LHS, RHS): # The index 1 corresponds to the governing equation for the second # state variable (temperature) LHS[1] += self.mass_rock * self.cp_rock ``` We can then use this new reactor class in a reactor network with some different values of `mass_rock` to see the impact of the modified governing equations. To do this, we can define a reactor network with an imposed heat transfer rate. ```{code-cell} python cp_rock = 790 # J/kg/K for mass_rock in [0.0, 1.0, 3.0]: # Define objects, properties, and initial conditions. gas = ct.Solution('h2o2.yaml') gas.TPX = 300, ct.one_atm, {'O2': 1, 'N2': 3.76} T0 = gas.T r1 = RockReactor(gas, mass_rock=mass_rock, cp_rock=cp_rock) r1.volume = 2.0 # volume (not including 'rock') env = ct.Reservoir(gas) wall = ct.Wall(env, r1, Q=1e4, A=1.0) # Heat transfer of 10 kW net = ct.ReactorNet([r1]) # Integrate for 10 seconds net.advance(10) print(f'mass_rock = {mass_rock} kg; ΔT = {r1.thermo.T - T0:.2f} K') ``` As expected, we see that the temperature rise decreases as the thermal mass of the reactor's contents increases. Other modifications to the reactor's governing equations and behavior may be made by implementing `before_*`, `after_`, and `replace_*` versions of any of the reactor methods listed in the documentation for the {py:class}`ExtensibleReactor` class. ## Additional Examples [Using `ExtensibleReactor` to implement wall inertia](/examples/python/reactors/custom2) : This example shows how to introduce additional governing equations to a reactor. Here, in addition to the `eval` method, overrides are introduced that modify the behavior of the `initialize`, `get_state`, `update_state`, `component_index`, and `component_name` methods. The result is a reactor network with a wall between two reactors that has inertia and takes time to respond to changes in the reactor's pressure. [Reactor cascade model for reactive flows in inert porous media](/examples/python/reactors/PorousMediaBurner) : This example showcases the use of {py:class}`ExtensibleReactor` to add a temperature equation for a solid phase and custom heat transfer/radiation models. Each reactor in the network represents a different spatial locations along the length of a porous media burner.