.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "examples/reactor_network/PSRnetwork.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_examples_reactor_network_PSRnetwork.py: .. _ref_equivalent_reactor_network: ============================================================================== Simulate a combustor using an equivalent reactor network with stream recycling ============================================================================== This example shows how to set up and solve an ERN (equivalent reactor network) in PyChemkin. An ERN is employed as a reduced-order model to simulate the steady-state combustion process inside a gas turbine combustor chamber. This reduced-order reactor network model retains the complexity of the combustion chemistry by sacrificing details of the combustor geometries, the spatial resolution, and the mass and energy transfer processes. An ERN usually comprises PSRs (perfectly-stirred reactors) and PFRs (plug-flow reactors). The network configuration and connectivity, the reactor parameters, and the mass flow rates can be determined from "hot" steady-state CFD simulation results and/or from observations and measured data of the actual/similar devices. Once a reactor network is calibrated against the experimental data of a gas combustor, it becomes a handy tool for quickly estimating the emissions from the combustor when it is subjected to certain variations in the fuel compositions. This figure shows a proposed ERN model of a fictional gas turbine combustor. .. figure:: combustor_ERN.png :scale: 80 % :alt: Combustor reactor network The *primary fuel* is mixed with the incoming air to form a *fuel-lean* mixture before entering the chamber through the primary inlet. Additional air, the *primary air*, is introduced to the combustion chamber separately through openings surrounding the primary inlet. The *secondary air* is entrained into the combustion chamber through well-placed holes on the liners at a location slightly downstream from the primary inlet. The first PSR (reactor #1) represents the *mixing zone* around the main injector where the cool *premixed fuel-air* stream and the *primary air* stream are preheated by mixing with the hot combustion products from the *recirculation zone*. Downstream from PSR #1, PSR #2, the *flame zone* is where the combustion of the heated fuel-air mixture takes place. The secondary air is injected here to cool down the combustion exhaust before it exits the combustion chamber. A portion of the exhaust gas coming out of PSR #2 does not leave the combustion chamber directly and is diverted to PSR #3, the *recirculation zone*. The majority of the outlet flow from PSR #3 is recirculated back to the flame zone to sustain the fuel-lean premixed flame there. The rest of the hot gas from PSR #3 travels further back to PSR #1, the mixing zone, to preheat the fuel-lean mixture just entering the combustion chamber. Finally, the cooled flue gas leaves the chamber in a stream-like manner. Typically, a PFR is applied to simulation of the outflow. The reactors in the network are solved individually one by one. When there is a *tear stream* in the network, the ERN should be solved iteratively. A tear stream is usually a *recycle" stream* that can serve as a pivot point for solving the recycle network iteratively. The convergence of the ERN is then determined by the absence of the per-iteration variation of the computed tear stream properties, such as species composition. In the current project, the recycling streams from PSR #3 to PSR #1 and PSR #2 are tear streams. .. GENERATED FROM PYTHON SOURCE LINES 83-85 .. code-block:: Python :dedent: 1 .. GENERATED FROM PYTHON SOURCE LINES 87-89 Import PyChemkin packages and start the logger =======================================+====== .. GENERATED FROM PYTHON SOURCE LINES 89-115 .. code-block:: Python 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 Mixture from ansys.chemkin.core.inlet import Stream # external gaseous inlet from ansys.chemkin.core.logger import logger # Chemkin PSR model (steady-state) from ansys.chemkin.core.stirreactors.PSR import PSRSetResTimeEnergyConservation as Psr import numpy as np # number crunching # check working directory current_dir = str(Path.cwd()) logger.debug("working directory: " + current_dir) # set verbose mode ck.set_verbose(True) # set interactive mode for plotting the results # interactive = True: display plot # interactive = False: save plot as a PNG file global interactive interactive = True .. GENERATED FROM PYTHON SOURCE LINES 116-121 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. .. GENERATED FROM PYTHON SOURCE LINES 121-132 .. code-block:: Python # 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") .. GENERATED FROM PYTHON SOURCE LINES 133-135 Preprocess the gasoline chemistry set ===================================== .. GENERATED FROM PYTHON SOURCE LINES 135-139 .. code-block:: Python # preprocess the mechanism files ierror = MyGasMech.preprocess() .. GENERATED FROM PYTHON SOURCE LINES 140-144 Set up gas mixtures based on the species in this chemistry set ============================================================== Create the "fuel" and the "air" mixtures to initialize the external inlet streams. The fuel for this case is pure methane. .. GENERATED FROM PYTHON SOURCE LINES 144-157 .. code-block:: Python # fuel is pure methane fuel = Mixture(MyGasMech) fuel.temperature = 650.0 # [K] fuel.pressure = 10.0 * ck.P_ATM # [atm] => [dyne/cm2] fuel.x = [("CH4", 1.0)] # air is modeled as a mixture of oxygen and nitrogen air = Mixture(MyGasMech) air.temperature = 650.0 # [K] air.pressure = 10.0 * ck.P_ATM air.x = ck.Air.x() # mole fractions .. GENERATED FROM PYTHON SOURCE LINES 158-176 Create external inlet streams from the mixtures =============================================== Create the ``fuel`` and the ``air`` streams/mixtures before setting up the external inlet streams. The fuel in this case is pure methane. The ``x_by_equivalence_ratio()`` method is used to form the main ``premixed`` inlet stream to the *mixing zone* reactor. The external inlets to the first reactor, the *mixing zone*, and the second reactor, the *flame zone*, are simply ``air`` mixtures with different mass flow rates and temperatures. .. note:: PyChemkin has *air* redefined as a convenient way to set up the air stream/mixture in the 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 oxygen and nitrogen are represented by ``o2`` and ``n2``. .. GENERATED FROM PYTHON SOURCE LINES 176-224 .. code-block:: Python # primary fuel-air mixture # products from the complete combustion of the fuel mixture and air products = ["CO2", "H2O", "N2"] # species mole fractions of added/inert mixture. # (You can also create an additives mixture here.) add_frac = np.zeros(MyGasMech.kk, dtype=np.double) # no additives: all zeros # create the unburned fuel-air mixture premixed = Stream(MyGasMech) # mean equivalence ratio equiv = 0.6 ierror = premixed.x_by_equivalence_ratio( MyGasMech, fuel.x, air.x, add_frac, products, equivalenceratio=equiv ) # check fuel-oxidizer mixture creation status if ierror != 0: print("Error: Failed to create the fuel-oxidizer mixture.") exit() # list the composition of the unburned fuel-air mixture premixed.list_composition(mode="mole") # set premixed fuel-air inlet temperature and mass flow rate premixed.temperature = fuel.temperature premixed.pressure = fuel.pressure premixed.mass_flowrate = 500.0 # [g/sec] # primary air stream to mix with the primary fuel-air stream primary_air = Stream(MyGasMech, label="Primary_Air") primary_air.x = air.x primary_air.pressure = air.pressure primary_air.temperature = air.temperature primary_air.mass_flowrate = 50.0 # [g/sec] # secondary bypass air stream secondary_air = Stream(MyGasMech, label="Secondary_Air") secondary_air.x = air.x secondary_air.pressure = air.pressure secondary_air.temperature = 670.0 # [K] secondary_air.mass_flowrate = 100.0 # [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") .. GENERATED FROM PYTHON SOURCE LINES 225-244 Define reactors in the reactor network ====================================== Set up the PSR for each zone one by one with *external inlets only*. For PSRs, use the ``set_inlet()`` method to add the external inlets to the reactor. A PFR always requires one external inlet when it is instantiated. From upstream to downstream, they are ``premix zone``, ``flame zone``, and ``recirculation zone``. The ``recirculation zone`` does not have an 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. .. GENERATED FROM PYTHON SOURCE LINES 244-272 .. code-block:: Python # PSR #1: mixing zone mix = Psr(premixed, label="mixing zone") # use different guess temperature mix.set_estimate_conditions(option="TP", guess_temp=800.0) # set PSR residence time (sec): required for PSRSetResTimeEnergyConservation model mix.residence_time = 0.5 * 1.0e-3 # add external inlets mix.set_inlet(premixed) mix.set_inlet(primary_air) # PSR #2: flame zone flame = Psr(premixed, label="flame zone") # use the equilibrium state of the inlet gas mixture as the guessed solution flame.set_estimate_conditions(option="TP", guess_temp=1600.0) # set PSR residence time (sec): required for PSRSetResTimeEnergyConservation model flame.residence_time = 1.5 * 1.0e-3 # add external inlet flame.set_inlet(secondary_air) # PSR #3: recirculation zone recirculation = Psr(premixed, label="recirculation zone") # use the equilibrium state of the inlet gas mixture as the guessed solution recirculation.set_estimate_conditions(option="TP", guess_temp=1600.0) # set PSR residence time (sec): required for PSRSetResTimeEnergyConservation model recirculation.residence_time = 1.5 * 1.0e-3 .. GENERATED FROM PYTHON SOURCE LINES 273-290 Create the reactor network ========================== Create a hybrid reactor network named ``PSRnetwork`` and add the reactors one by one from upstream to downstream using the ``add_reactor()`` method. 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:: - Use the ``show_reactors()`` method to get the list of reactors in the network in the order that they are added. - Use the ``remove_reactor()`` method to remove an existing reactor from the network by the reactor ``name/label``. Similarly, use the ``clear_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. .. GENERATED FROM PYTHON SOURCE LINES 290-302 .. code-block:: Python # instantiate the PSR network as a hybrid reactor network PSRnetwork = ERN(MyGasMech) # add the reactors from upstream to downstream PSRnetwork.add_reactor(mix) PSRnetwork.add_reactor(flame) PSRnetwork.add_reactor(recirculation) # list the reactors in the network PSRnetwork.show_reactors() .. GENERATED FROM PYTHON SOURCE LINES 303-329 Define the PSR connectivity =========================== Because the current reactor network contains recycling streams, for example, from PSR #3 to PSR #1 and PSR #2, you must explicitly specify the network connectivity. To do this, you use a *split* dictionary to define the outflow connections among the PSRs in the network. The *key* of the split dictionary is the label of the *originate PSR*. The *value* is a list of tuples consisting of the label of the *target PSR* and its mass flow rate fraction. :: { "originate PSR" : [("target PSR1", fraction), ("target PSR2", fraction)], "another originate PSR" : [("target PSR1", fraction), ... ], ... } For example, the outflow from reactor ``PSR1``is split into two streams. 90% of the mass flow rate goes to ``PSR2`` and the rest is diverted to ``PSR5``. The split dictionary entry for the outflow split of ``PSR1`` is as follows: :: "PSR1" : [("PSR2", 0.9), ("PSR5", 0.1)] .. note:: The outlet flow from a reactor that is leaving the reactor network must be labeled as ``EXIT>>`` when you define the outflow splitting. .. GENERATED FROM PYTHON SOURCE LINES 329-343 .. code-block:: Python # PSR #1 outlet flow splitting split_table = [(flame.label, 1.0)] PSRnetwork.add_outflow_connections(mix.label, split_table) # PSR #2 outlet flow splitting # part of the outlet flow from PSR #2 exits the reactor network split_table = [(recirculation.label, 0.2), ("EXIT>>", 0.8)] PSRnetwork.add_outflow_connections(flame.label, split_table) # PSR #3 outlet flow splitting # PSR #3 is the last reactor of the network. However, # it does not have an external outlet. split_table = [(mix.label, 0.15), (flame.label, 0.85)] PSRnetwork.add_outflow_connections(recirculation.label, split_table) .. GENERATED FROM PYTHON SOURCE LINES 344-357 Define the tear point for the iteration ======================================= Because the reactor network contains recycling streams, it must be solved iteratively by applying the *tear stream* method. You use the ``add_tearingpoint() `` method to explicitly define the *tear points* of the network. In this example, the stream recycling occurs at reactor #3, where the outlet flow is sent back to the upstream reactors, reactor #1 and reactor #2. Therefore, reactor #3, the *recirculation zone*, should be set as the only tear point of this reactor network. You use the ``set_tear_tolerance()`` method to set the relative tolerance for the overall reactor network convergence. The default tolerance is 1.0e-6. .. GENERATED FROM PYTHON SOURCE LINES 357-364 .. code-block:: Python # define the tear point reactor as reactor #3 PSRnetwork.add_tearingpoint(recirculation.label) # reset the network relative tolerance PSRnetwork.set_tear_tolerance(1.0e-5) .. GENERATED FROM PYTHON SOURCE LINES 365-382 Solve the reactor network ========================= Use the ``run()`` method to solve the entire reactor network. The ``ReactorNetwork`` hybrid solves the reactors one by one in the order that they are added to the network. .. note:: Use the ``set_tear_iteration_limit(count)`` method to change the limit on the number of tear loop iterations that can be taken before declaring failure. The default limit is 200. .. note:: The ``set_relaxation_factor(factor)`` method can be employed to make solution updating at the end of each iteration to be more aggressive (factor > 1.0) or more conservative (factor < 1.0). By default, the relaxation factor is set to 1. .. GENERATED FROM PYTHON SOURCE LINES 382-397 .. code-block:: Python # set the start wall time start_time = time.time() # solve the reactor network iteratively status = PSRnetwork.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].") print() .. GENERATED FROM PYTHON SOURCE LINES 398-412 Postprocess the reactor network solutions ========================================= There are two ways to process the 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 with the reactor name. Or, you can get the stream properties of a specific network outlet by using the ``get_external_stream()`` method with the 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 exists of the reactor network. .. GENERATED FROM PYTHON SOURCE LINES 412-451 .. code-block:: Python # 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 = {PSRnetwork.number_external_outlets}.") # display the external outlet stream properties for m in range(PSRnetwork.number_external_outlets): n = m + 1 network_outflow = PSRnetwork.get_external_stream(n) # set the stream label network_outflow.label = "outflow" # print the desired outlet stream properties print("=" * 10) print(f"outflow # {n}") 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]}.") print("-" * 10) # display the reactor solutions print() print("=" * 10) print("reactor/zone") print("=" * 10) for index, stream in PSRnetwork.reactor_solutions.items(): name = PSRnetwork.get_reactor_label(index) print(f"Reactor: {name}.") print(f"Temperature = {stream.temperature} [K].") print(f"Mass flow rate = {stream.mass_flowrate} [g/sec].") print(f"CH4 = {stream.x[ch4_index]}.") print(f"O2 = {stream.x[o2_index]}.") print(f"CO = {stream.x[co_index]}.") print(f"NO = {stream.x[no_index]}.") print("-" * 10) .. _sphx_glr_download_examples_reactor_network_PSRnetwork.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: PSRnetwork.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: PSRnetwork.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: PSRnetwork.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_