Note
Go to the end to download the full example code.
A PSR cluster with heat exchange#
This example shows how to set up and solve a series of linked PSRs (perfectly-stirred reactors) with heat exchange.
Here is a PSR chain model of a fictional gas combustor with heat exchange:
The primary fuel inlet stream to the first reactor, the pre-heater, contains pure methane. The outlet flow from the pre-heater enters the second reactor, the mixer, where the hot fuel would mix with the cool air from the external inlet to form a combustible mathen-air mixture. The combustible mixture from the mixer then travel to the third reactor, the combustor in which the methane-air mixture would ignite and burn. The combustor is connected to the pre-heater by a heat exchanger so that a portion of the heat generated from the combustion in the combustor will be recycled to the pre-heeater to heat up the fuel stream.
This example uses the ReactorNetwork module to configure and solve
this chain reactor network with heat exchange iteratively. 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.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,
)
from ansys.chemkin.core.stirreactors.PSRcluster import PSRCluster as Ern
# 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.Air.x() or
ansys.chemkin.Air.y() method when the mechanism uses “O2” and “N2” for
oxygen and nitrogen. Use the ansys.chemkin.Air.x('L') or
ansys.chemkin.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 = 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 = 300.0 # [K]
air.pressure = ck.P_ATM
# use predefined "air" recipe in mole fractions (with upper cased symbols)
air.x = ck.Air.x()
air.mass_flowrate = 66.75 # [g/sec]
# prepare a premixed stream as the guessed condition for the combustor
premixed = adiabatic_mixing_streams(fuel, air)
# 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: pre-heater
preheater = Psr(air, label="pre-heater")
# set PSR residence time (sec): required for PSR_SetResTime_EnergyConservation model
preheater.residence_time = 1.5 * 1.0e-3
# add external inlet
preheater.set_inlet(air)
# PSR #2: mixer
mixer = Psr(fuel, label="mixer")
# set PSR residence time (sec): required for PSR_SetResTime_EnergyConservation model
mixer.residence_time = 0.5 * 1.0e-3
# add external inlet
mixer.set_inlet(fuel)
# PSR #3: combustor
# use the premixed methane-air mixture to set up the combustor
combustor = Psr(premixed, label="combustor")
# use the equilibrium state of the premixed methane-air mixture as the guessed solution
combustor.set_estimate_conditions(option="HP")
# set PSR residence time (sec): required for PSR_SetResTime_EnergyConservation model
combustor.residence_time = 4.0 * 1.0e-3
Create the reactor network#
Create a coupled reactor network named PSRcluster with the list of the PSRs
in the correct order. For a simple chain network, you do not
need to define the connectivity among the reactors. The reactor network model
automatically figures out the through-flow connections.
Note
The order of the reactor in the list is important as it dictates the solution sequence and thus the convergence rate.
# instantiate the PSR network as a coupled reactor network
PSR_list = [preheater, mixer, combustor]
PSRcluster = Ern(PSR_list, label="combustor_cluster")
Add the heat transfer connection#
Use add_heat_exchange() method to define a heat exchange connection
between the pre-heater and the combustor.
# effecctive heat transfer coefficient of the heat exchanger [cal/cm2-K-sec]
ht_coeff = 0.0025
# effective surface area of the heat exchanger [cm2]
ht_area = 100.0
PSRcluster.add_heat_exchange(preheater.label, combustor.label, ht_coeff, ht_area)
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 = PSRcluster.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 the reactor network solutions#
Since the reactors in the same cluster are solved together,
you must get use the process_cluster_solution() method first,
and extract the solution stream of a specific PSR outlet by using the
get_reactor_stream() method with the (1-based) reactor index.
Once you get the solution as a stream, you can use any stream or mixture
method to further manipulate the solutions.
# postprocess the solutions of all PSRs in the cluster
ierr = PSRcluster.process_cluster_solution()
if ierr == 0:
# verify the mass flow rate in and out of the PSR cluster
print(
f"net external inlet mass flow rate = "
f"{PSRcluster.total_inlet_mass_flow_rate} [g/sec]."
)
print(
f"net outlet mass flow rate = "
f"{PSRcluster.get_cluster_outlet_flowrate()} [g/sec]."
)
else:
print("Failed to post-process the network solution.")
print(f"error code = {ierr}")
exit()
# display the reactor solutions
print("=" * 10)
print("reactor/zone")
print("=" * 10)
for index in range(PSRcluster.numb_psrs):
id = index + 1
name = PSRcluster.get_reactor_label(id)
# get the solution stream of the reactor
sstream = PSRcluster.get_reactor_stream(name)
print(f"Reactor: {name}.")
print(f"Temperature = {sstream.temperature} [K].")
print(f"Mass flow rate = {sstream.mass_flowrate} [g/sec].")
print(f"CH4 = {sstream.x[ch4_index]}.")
print(f"O2 = {sstream.x[o2_index]}.")
print(f"CO = {sstream.x[co_index]}.")
print(f"NO = {sstream.x[no_index]}.")
print("-" * 10)
psr1_mixture = PSRcluster.get_reactor_stream("pre-heater")
psr2_mixture = PSRcluster.get_reactor_stream("mixer")
print(f"pre-heater temperature = {psr1_mixture.temperature} [K]")
print(f"mixer temperature = {psr2_mixture.temperature} [K]")
# clean up
ck.done()