Note
Go to the end to download the full example code.
Use a chain reactor network to model a gas combustor#
This example shows how to set up and solve a series of linked PSRs (perfectly-stirred reactors). This is the simplest reactor network as it does not contain any recycling streams or outflow splittings.
Here is a PSR chain model of a fictional gas combustor:
The primary inlet stream to the first reactor, the combustor, is the fuel-lean methane-air mixture that is formed by mixing the fuel (methane) and the heated air. The exhaust from the combustor enters the second reactor, the dilution zone, where the hot combustion products are cooled by the introduction of additional cool air. The cooled and diluted gas mixture in the dilution zone then travel to the third reactor, the reburning zone. A mixture of fuel (methane) and carbon dioxide is injected to the gas in the reburning zone, attempting to convert any remaining carbon monoxide or nitric oxide in the exhaust gas to carbon dioxide or nitrogen, respectively.
This example uses the ReactorNetwork module to configure and solve
this chain reactor network. This module automatically handles the tasks of
running the individual reactors and setting up the inlet to the downstream reactor.
Import PyChemkin packages and start the logger#
from pathlib import Path
import time
import ansys.chemkin.core as ck # Chemkin
from ansys.chemkin.core import Color
from ansys.chemkin.core.hybridreactornetwork import ReactorNetwork as ERN
from ansys.chemkin.core.inlet import Stream # external gaseous inlet
from ansys.chemkin.core.inlet import adiabatic_mixing_streams
from ansys.chemkin.core.logger import logger
# Chemkin PSR model (steady-state)
from ansys.chemkin.core.stirreactors.PSR import PSRSetResTimeEnergyConservation as Psr
# check working directory
current_dir = str(Path.cwd())
logger.debug("working directory: " + current_dir)
# set verbose mode
ck.set_verbose(True)
Create a chemistry set#
The mechanism to load is the GRI 3.0 mechanism for methane combustion.
This mechanism and its associated data files come with the standard Ansys Chemkin
installation in the /reaction/data directory.
# set mechanism directory (the default Chemkin mechanism data directory)
data_dir = Path(ck.ansys_dir) / "reaction" / "data"
mechanism_dir = data_dir
# create a chemistry set based on the GRI mechanism
MyGasMech = ck.Chemistry(label="GRI 3.0")
# set mechanism input files
# including the full file path is recommended
MyGasMech.chemfile = str(mechanism_dir / "grimech30_chem.inp")
MyGasMech.thermfile = str(mechanism_dir / "grimech30_thermo.dat")
Preprocess the gasoline chemistry set#
# preprocess the mechanism files
ierror = MyGasMech.preprocess()
Set up gas mixtures based on the species in this chemistry set#
Create the fuel and air streams before setting up the
external inlet streams. The fuel in this case is pure methane. The main
premixed inlet stream to the combustor is formed by mixing the
fuel and air streams adiabatically. The fuel-to-air mass ratio
is provided implicitly by the mass flow rates of the two streams. The
external inlet to the second reactor, the dilution zone, is simply the
air stream with a different mass flow rate (and different temperature
if desirable). The reburn_fuel stream to inject to the downstream
reburning zone is a mixture of methane and carbon dioxide.
Note
PyChemkin has air predefined as a convenient way to set up the air
stream/mixture in simulations. Use the ansys.chemkin.core.Air.x() or
ansys.chemkin.core.Air.y() method when the mechanism uses “O2” and “N2” for
oxygen and nitrogen. Use the ansys.chemkin.core.Air.x('L') or
ansys.chemkin.core.Air.y('L') method when the mechanism uses “o2” and “n2”
for oxygen and nitrogen.
# fuel is pure methane
fuel = Stream(MyGasMech)
fuel.temperature = 300.0 # [K]
fuel.pressure = 2.1 * ck.P_ATM # [atm] => [dyne/cm2]
fuel.x = [("CH4", 1.0)]
fuel.mass_flowrate = 3.275 # [g/sec]
# air is modeled as a mixture of oxygen and nitrogen
air = Stream(MyGasMech)
air.temperature = 550.0 # [K]
air.pressure = 2.1 * ck.P_ATM
# use predefined "air" recipe in mole fractions (with upper cased symbols)
air.x = ck.Air.x()
air.mass_flowrate = 45.0 # [g/sec]
Create external inlet streams from the mixtures#
Use the adiabatic_mixing_streams() method to combine
the fuel and the air streams. The final gas temperature should
land between the temperatures of the two source streams. The mass flow
rate of the premixed stream should be the sum of the sources.
Use a simple PyChemkin composition recipe to create
the reburn_fuel stream.
# premixed stream for the combustor
premixed = adiabatic_mixing_streams(fuel, air)
# verify the premixed stream properties
print(f"Premixed stream temperature = {premixed.temperature} [K].")
print(f"Premixed stream mass flow rate = {premixed.mass_flowrate} [g/sec].")
# additional fuel injection for the reburning zone
reburn_fuel = Stream(MyGasMech)
reburn_fuel.temperature = 300.0 # [K]
reburn_fuel.pressure = 2.1 * ck.P_ATM # [atm] => [dyne/cm2]
reburn_fuel.x = [("CH4", 0.6), ("CO2", 0.4)]
reburn_fuel.mass_flowrate = 0.12 # [g/sec]
# find the species index
ch4_index = MyGasMech.get_specindex("CH4")
o2_index = MyGasMech.get_specindex("O2")
no_index = MyGasMech.get_specindex("NO")
co_index = MyGasMech.get_specindex("CO")
Create PSRs for each zone#
Set up the PSR for each zone one by one with external inlets only.
For PSR creation, use the set_inlet() method to add the external inlets to the
reactor. A PFR always requires one external inlet when it is instantiated.
There are three reactors in the network. From upstream to downstream, they
are combustor, dilution zone, and reburning zone. All of them
have one external inlet.
Note
PyChemkin requires that the first reactor/zone must have at least one external inlet. Because the rest of the reactors have at least the through flow from the immediate upstream reactor, they do not require an external inlet.
Note
The Stream parameter used to instantiate a PSR is used to establish
the guessed reactor solution and is modified when the network is solved by
the ERN.
# PSR #1: combustor
combustor = Psr(premixed, label="combustor")
# use the equilibrium state of the inlet gas mixture as the guessed solution
combustor.set_estimate_conditions(option="HP")
# set PSR residence time (sec): required for PSRSetResTimeEnergyConservation model
combustor.residence_time = 2.0 * 1.0e-3
# add external inlet
combustor.set_inlet(premixed)
# PSR #2: dilution zone
dilution = Psr(premixed, label="dilution zone")
# set PSR residence time (sec): required for PSRSetResTimeEnergyConservation model
dilution.residence_time = 1.5 * 1.0e-3
# add external inlet
# assign the correct mass flow rate to the "air" stream
air.mass_flowrate = 62.0 # [g/sec]
dilution.set_inlet(air)
# PSR #3: reburning zone
reburn = Psr(premixed, label="reburning zone")
# set PSR residence time (sec): required for PSRSetResTimeEnergyConservation model
reburn.residence_time = 3.5 * 1.0e-3
# add external inlet
reburn.set_inlet(reburn_fuel)
Create the reactor network#
Create a hybrid reactor network named PSRChain and use the add_reactor()
method to add the reactors one by one from upstream to downstream. For a
simple chain network such as the one used in this example, you do not
need to define the connectivity among the reactors. The reactor network model
automatically figures out the through-flow connections.
Note
Use the
show_reactors()method to get the list of reactors in the network in the order they are added.Use the
remove_reactor()method to remove an existing reactor from the network by the reactorname/label. Similarly, use theclear_connections()method to undo the network connectivity.The order of the reactor addition is important as it dictates the solution
sequence and thus the convergence rate.
# instantiate the chain PSR network as a hybrid reactor network
PSRChain = ERN(MyGasMech)
# add the reactors from upstream to downstream
PSRChain.add_reactor(combustor)
PSRChain.add_reactor(dilution)
PSRChain.add_reactor(reburn)
# list the reactors in the network
PSRChain.show_reactors()
Solve the reactor network#
Use the run() method to solve the entire reactor network.
The hybrid reactor network solves the reactors one by one in the order
that they are added to the network.
# set the start wall time
start_time = time.time()
# solve the reactor network
status = PSRChain.run()
if status != 0:
print(Color.RED + "Failed to solve the reactor network." + Color.END)
exit()
# compute the total runtime
runtime = time.time() - start_time
print()
print(f"Total simulation duration: {runtime} [sec]")
Postprocess reactor network results#
There are two ways to process results from a reactor network. You
can extract the solution of an individual reactor member as a stream
by using the get_reactor_stream() method to get the reactor by its name. Or,
you can get the stream properties of a specific network outlet by using the
get_external_stream() method to get the stream by its outlet index. Once
you get the solution as a stream, you can use any stream or mixture
method to further manipulate the solutions.
Note
Use the number_external_outlets() method to find out the number of
external outlets of the reactor network.
# get the outlet stream from the reactor network solutions
# find the number of external outlet streams from the reactor network
print(f"Number of outlet streams = {PSRChain.number_external_outlets}.")
# get the first (and the only) external outlet stream properties
network_outflow = PSRChain.get_external_stream(1)
# set the stream label
network_outflow.label = "outflow"
# print the desired outlet stream properties
print()
print("=" * 10)
print("outflow")
print("=" * 10)
print(f"temperature = {network_outflow.temperature} [K]")
print(f"mass flow rate = {network_outflow.mass_flowrate} [g/sec]")
print(f"CH4 = {network_outflow.x[ch4_index]}")
print(f"O2 = {network_outflow.x[o2_index]}")
print(f"CO = {network_outflow.x[co_index]}")
print(f"NO = {network_outflow.x[no_index]}")