Source code for chemicals.combustion

"""Chemical Engineering Design Library (ChEDL). Utilities for process modeling.
Copyright (C) 2016, 2017, 2018, 2019, 2020 Caleb Bell <Caleb.Andrew.Bell@gmail.com>
Copyright (C) 2020 Yoel Rene Cortes-Pena <yoelcortes@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


This module contains a series of functions for modeling combustion reactions.

For reporting bugs, adding feature requests, or submitting pull requests,
please use the `GitHub issue tracker <https://github.com/CalebBell/chemicals/>`_.

.. contents:: :local:

Combustion Stoichiometry
------------------------
.. autofunction:: chemicals.combustion.combustion_stoichiometry
.. autofunction:: chemicals.combustion.combustion_products_mixture

Heat of Combustion
------------------
.. autofunction:: chemicals.combustion.HHV_stoichiometry
.. autofunction:: chemicals.combustion.HHV_modified_Dulong
.. autofunction:: chemicals.combustion.LHV_from_HHV

Heat of Combustion and Stiochiometry
------------------------------------
.. autofunction:: chemicals.combustion.combustion_data
.. autoclass:: chemicals.combustion.CombustionData

Basic Combustion Spec Solvers
-----------------------------
.. autofunction:: chemicals.combustion.fuel_air_spec_solver
.. autofunction:: chemicals.combustion.combustion_spec_solver
.. autofunction:: chemicals.combustion.air_fuel_ratio_solver


Engine Combustion
-----------------
.. autofunction:: chemicals.combustion.Perez_Boehman_RON_from_ignition_delay
.. autofunction:: chemicals.combustion.Perez_Boehman_MON_from_ignition_delay
.. autofunction:: chemicals.combustion.octane_sensitivity
.. autofunction:: chemicals.combustion.AKI
.. autofunction:: chemicals.combustion.IDT_to_DCN

Lookup Functions
----------------
.. autofunction:: chemicals.combustion.RON
.. autofunction:: chemicals.combustion.RON_methods
.. autodata:: chemicals.combustion.RON_all_methods
.. autofunction:: chemicals.combustion.MON
.. autofunction:: chemicals.combustion.MON_methods
.. autodata:: chemicals.combustion.MON_all_methods
.. autofunction:: chemicals.combustion.ignition_delay
.. autofunction:: chemicals.combustion.ignition_delay_methods
.. autodata:: chemicals.combustion.ignition_delay_all_methods

"""

from fluids.numerics import normalize

from chemicals import data_reader as dr
from chemicals.data_reader import (
    data_source,
    database_constant_lookup,
    list_available_methods_from_df_dict,
    register_df_source,
    retrieve_any_from_df_dict,
    retrieve_from_df_dict,
)
from chemicals.elements import mass_fractions, molecular_weight, simple_formula_parser
from chemicals.utils import PY37, can_load_data, mark_numba_incompatible, os_path_join, property_mass_to_molar, property_molar_to_mass, source_path

__all__ = ('combustion_stoichiometry',
           'CombustionData',
           'combustion_data',
           'HHV_modified_Dulong',
           'HHV_stoichiometry',
           'LHV_from_HHV',
           'combustion_products_mixture',
           'air_fuel_ratio_solver',
           'fuel_air_spec_solver',
           'combustion_spec_solver',
           'RON', 'RON_methods',
           'MON', 'MON_methods',
           'Perez_Boehman_RON_from_ignition_delay',
           'Perez_Boehman_MON_from_ignition_delay',
           'octane_sensitivity',
           'AKI',
           'ignition_delay_all_methods', 'ignition_delay_methods',
           'ignition_delay',
           'IDT_to_DCN')


# Register data sources and lazy load them

folder = os_path_join(source_path, 'Misc')
register_df_source(folder, 'Florian_Liming_RON_experimental.tsv')
register_df_source(folder, 'Florian_Liming_MON_experimental.tsv')
register_df_source(folder, 'Florian_Liming_RON_MON_ANN.tsv')
register_df_source(folder, 'Travis_Kessler_Combustdb_RON.tsv')
register_df_source(folder, 'Travis_Kessler_Combustdb_MON.tsv')
register_df_source(folder, 'Travis_Kessler_Combustdb_predictions.tsv')
register_df_source(folder, 'Dahmen_Marquardt_ignition_delay_IQT.tsv')


FLORIAN_LIMING = 'FLORIAN_LIMING'
FLORIAN_LIMING_ANN = 'FLORIAN_LIMING_ANN'
COMBUSTDB = 'COMBUSTDB'
COMBUSTDB_PREDICTIONS = 'COMBUSTDB_PREDICTIONS'
DAHMEN_MARQUARDT = 'DAHMEN_MARQUARDT'


_combustion_data_loaded = False
@mark_numba_incompatible
def _load_combustion_data():
    global _combustion_data_loaded, florian_liming_ron_experimental, RON_sources, combustdb_ron, combustdb_predictions
    global florian_liming_ron_mon_ann, florian_liming_mon_experimental, MON_sources, dahmen_marquardt_iqt
    global ignition_delay_sources
    florian_liming_ron_experimental = data_source('Florian_Liming_RON_experimental.tsv')
    florian_liming_mon_experimental = data_source('Florian_Liming_MON_experimental.tsv')
    florian_liming_ron_mon_ann = data_source('Florian_Liming_RON_MON_ANN.tsv')
    combustdb_ron = data_source('Travis_Kessler_Combustdb_RON.tsv')
    combustdb_mon = data_source('Travis_Kessler_Combustdb_MON.tsv')
    combustdb_predictions = data_source('Travis_Kessler_Combustdb_predictions.tsv')
    dahmen_marquardt_iqt = data_source('Dahmen_Marquardt_ignition_delay_IQT.tsv')

    RON_sources = {
        FLORIAN_LIMING: florian_liming_ron_experimental,
        COMBUSTDB: combustdb_ron,
        COMBUSTDB_PREDICTIONS: combustdb_predictions,
        FLORIAN_LIMING_ANN: florian_liming_ron_mon_ann,
    }
    MON_sources = {
        FLORIAN_LIMING: florian_liming_mon_experimental,
        COMBUSTDB: combustdb_mon,
        COMBUSTDB_PREDICTIONS: combustdb_predictions,
        FLORIAN_LIMING_ANN: florian_liming_ron_mon_ann,
    }

    ignition_delay_sources = {
        DAHMEN_MARQUARDT: dahmen_marquardt_iqt,
    }

if PY37:
    def __getattr__(name):
        if name in ('florian_liming_ron_experimental',
                    'florian_liming_ron_mon_ann',
                    'combustdb_ron',
                    'combustdb_predictions',
                    'dahmen_marquardt_iqt'
                    'RON_sources',
                    'MON_sources',
                    'ignition_delay_sources'):
            _load_combustion_data()
            return globals()[name]
        raise AttributeError(f"module {__name__} has no attribute {name}")
else:
    if can_load_data:
        _load_combustion_data()


RON_all_methods = (FLORIAN_LIMING, COMBUSTDB, FLORIAN_LIMING_ANN, COMBUSTDB_PREDICTIONS)
"""Tuple of method name keys. See the `RON` for the actual references"""

[docs]@mark_numba_incompatible def RON_methods(CASRN): """Return all methods available to obtain the research octane number (RON) for the desired chemical. Parameters ---------- CASRN : str CASRN, [-] Returns ------- methods : list[str] Methods which can be used to obtain the RON with the given inputs. See Also -------- RON """ if not _combustion_data_loaded: _load_combustion_data() return list_available_methods_from_df_dict(RON_sources, CASRN, 'RON')
[docs]@mark_numba_incompatible def RON(CASRN, method=None): r'''This function handles the retrieval of a chemical's research octane number (RON). Lookup is based on CASRNs. Will automatically select a data source to use if no method is provided; returns None if the data is not available. Function has data for approximately 1400 chemicals. Parameters ---------- CASRN : str CASRN [-] Returns ------- RON : float Research octane number, [-] Other Parameters ---------------- method : string, optional A string for the method name to use, as defined by constants in RON_methods Notes ----- The available sources are as follows: * 'FLORIAN_LIMING', the experimental values compiled in [1]_. * 'FLORIAN_LIMING_ANN', a set of predicted values using a QSPR-ANN model developed in the author's earlier publication [3]_, from 260 comonents. * 'COMBUSTDB', a compilation of values from various sources [2]_. * 'COMBUSTDB_PREDICTIONS', a set of predicted values developed by the author of CombustDB (Travis Kessler) using the tool [4]_. Examples -------- >>> RON(CASRN='64-17-5') 108.6 References ---------- .. [1] Lehn, Florian vom, Liming Cai, Rupali Tripathi, Rafal Broda, and Heinz Pitsch. "A Property Database of Fuel Compounds with Emphasis on Spark-Ignition Engine Applications." Applications in Energy and Combustion Science 5 (March 1, 2021): 100018. https://doi.org/10.1016/j.jaecs.2020.100018. .. [2] Kessler, Travis. CombustDB. Python. 2019. UMass Lowell Energy and Combustion Research Laboratory, 2021. https://github.com/ecrl/combustdb. .. [3] Lehn, Florian vom, Benedict Brosius, Rafal Broda, Liming Cai, and Heinz Pitsch. "Using Machine Learning with Target-Specific Feature Sets for Structure-Property Relationship Modeling of Octane Numbers and Octane Sensitivity." Fuel 281 (December 1, 2020): 118772. https://doi.org/10.1016/j.fuel.2020.118772. .. [4] Kessler, Travis, and John Hunter Mack. "ECNet: Large Scale Machine Learning Projects for Fuel Property Prediction." Journal of Open Source Software 2, no. 17 (2017): 401. ''' if dr.USE_CONSTANTS_DATABASE and method is None: val, found = database_constant_lookup(CASRN, 'RON') if found: return val if not _combustion_data_loaded: _load_combustion_data() if method: value = retrieve_from_df_dict(RON_sources, CASRN, 'RON', method) else: value = retrieve_any_from_df_dict(RON_sources, CASRN, 'RON') return value
MON_all_methods = (FLORIAN_LIMING, COMBUSTDB, FLORIAN_LIMING_ANN, COMBUSTDB_PREDICTIONS) """Tuple of method name keys. See the `MON` for the actual references"""
[docs]@mark_numba_incompatible def MON_methods(CASRN): """Return all methods available to obtain the motor octane number (MON) for the desired chemical. Parameters ---------- CASRN : str CASRN, [-] Returns ------- methods : list[str] Methods which can be used to obtain the MON with the given inputs. See Also -------- MON """ if not _combustion_data_loaded: _load_combustion_data() return list_available_methods_from_df_dict(MON_sources, CASRN, 'MON')
[docs]@mark_numba_incompatible def MON(CASRN, method=None): r'''This function handles the retrieval of a chemical's motor octane number (MON). Lookup is based on CASRNs. Will automatically select a data source to use if no method is provided; returns None if the data is not available. Function has data for approximately 1400 chemicals. Parameters ---------- CASRN : str CASRN [-] Returns ------- MON : float Research octane number, [-] Other Parameters ---------------- method : string, optional A string for the method name to use, as defined by constants in MON_methods Notes ----- The available sources are as follows: * 'FLORIAN_LIMING', the experimental values compiled in [1]_. * 'FLORIAN_LIMING_ANN', a set of predicted values using a QSPR-ANN model developed in the author's earlier publication [3]_, from 260 comonents. * 'COMBUSTDB', a compilation of values from various sources [2]_. * 'COMBUSTDB_PREDICTIONS', a set of predicted values developed by the author of CombustDB (Travis Kessler) using the tool [4]_. Examples -------- >>> MON(CASRN='64-17-5') 89.7 References ---------- .. [1] Lehn, Florian vom, Liming Cai, Rupali Tripathi, Rafal Broda, and Heinz Pitsch. "A Property Database of Fuel Compounds with Emphasis on Spark-Ignition Engine Applications." Applications in Energy and Combustion Science 5 (March 1, 2021): 100018. https://doi.org/10.1016/j.jaecs.2020.100018. .. [2] Kessler, Travis. CombustDB. Python. 2019. UMass Lowell Energy and Combustion Research Laboratory, 2021. https://github.com/ecrl/combustdb. .. [3] Lehn, Florian vom, Benedict Brosius, Rafal Broda, Liming Cai, and Heinz Pitsch. "Using Machine Learning with Target-Specific Feature Sets for Structure-Property Relationship Modeling of Octane Numbers and Octane Sensitivity." Fuel 281 (December 1, 2020): 118772. https://doi.org/10.1016/j.fuel.2020.118772. .. [4] Kessler, Travis, and John Hunter Mack. "ECNet: Large Scale Machine Learning Projects for Fuel Property Prediction." Journal of Open Source Software 2, no. 17 (2017): 401. ''' if dr.USE_CONSTANTS_DATABASE and method is None: val, found = database_constant_lookup(CASRN, 'MON') if found: return val if not _combustion_data_loaded: _load_combustion_data() if method: value = retrieve_from_df_dict(MON_sources, CASRN, 'MON', method) else: value = retrieve_any_from_df_dict(MON_sources, CASRN, 'MON') return value
ignition_delay_all_methods = (DAHMEN_MARQUARDT,) """Tuple of method name keys. See the `ignition_delay` for the actual references"""
[docs]@mark_numba_incompatible def ignition_delay_methods(CASRN): """Return all methods available to obtain the ignition delay time (IDT) for the desired chemical. Parameters ---------- CASRN : str CASRN, [-] Returns ------- methods : list[str] Methods which can be used to obtain the IDT with the given inputs. See Also -------- ignition_delay """ if dr.USE_CONSTANTS_DATABASE: val, found = database_constant_lookup(CASRN, 'IGNITION_DELAY') if found: return val if not _combustion_data_loaded: _load_combustion_data() return list_available_methods_from_df_dict(ignition_delay_sources, CASRN, 'IGNITION_DELAY')
[docs]@mark_numba_incompatible def ignition_delay(CASRN, method=None): r'''This function handles the retrieval of a chemical's ignition delay time (IDT). Lookup is based on CASRNs. Will automatically select a data source to use if no method is provided; returns None if the data is not available. Function has data for approximately 60 chemicals. Parameters ---------- CASRN : str CASRN [-] Returns ------- ignition_delay : float Ignition delay time, [s] Other Parameters ---------------- method : string, optional A string for the method name to use, as defined by constants in ignition_delay_all_methods Notes ----- The available sources are as follows: * 'DAHMEN_MARQUARDT', the experimental values compiled in [1]_; all timings come from the IQT tester device Note that different measurement devices can give different results. Examples -------- >>> ignition_delay(CASRN='110-54-3') 0.0043 References ---------- .. [1] Dahmen, Manuel, and Wolfgang Marquardt. "A Novel Group Contribution Method for the Prediction of the Derived Cetane Number of Oxygenated Hydrocarbons." Energy & Fuels 29, no. 9 (September 17, 2015): 5781-5801. https://doi.org/10.1021/acs.energyfuels.5b01032. ''' if not _combustion_data_loaded: _load_combustion_data() if method: value = retrieve_from_df_dict(ignition_delay_sources, CASRN, 'IGNITION_DELAY', method) else: value = retrieve_any_from_df_dict(ignition_delay_sources, CASRN, 'IGNITION_DELAY') return value
[docs]def AKI(RON, MON): r'''This function calculates the anti knock index (AKI) of a fuel, also known as (R+M)/2 and by DON [1]_. .. math:: \text{AKI} = 0.5\text{RON} + 0.5\text{MON} Parameters ---------- RON : float Research octane number, [-] MON : float Motor octane number, [-] Returns ------- AKI : float Average of RON and MON, [-] Notes ----- This is the number displayed at the gas pumps in North America; in Europe and Asia the RON is displayed. Examples -------- >>> AKI(RON=90, MON=74) 82.0 References ---------- .. [1] McKinsey. "Octane." Accessed April 18, 2022. http://www.mckinseyenergyinsights.com/resources/refinery-reference-desk/octane/. ''' return 0.5*(RON + MON)
[docs]def octane_sensitivity(RON, MON): r'''This function calculates the octane sensitivity of a fuel [1]_. .. math:: \text{OS} = \text{RON} - \text{MON} Parameters ---------- RON : float Research octane number, [-] MON : float Motor octane number, [-] Returns ------- OS : float Octane sensitivity, [-] Notes ----- Examples -------- >>> octane_sensitivity(RON=90, MON=74) 16 References ---------- .. [1] Lehn, Florian vom, Liming Cai, Rupali Tripathi, Rafal Broda, and Heinz Pitsch. "A Property Database of Fuel Compounds with Emphasis on Spark-Ignition Engine Applications." Applications in Energy and Combustion Science 5 (March 1, 2021): 100018. https://doi.org/10.1016/j.jaecs.2020.100018. ''' return RON - MON
[docs]def Perez_Boehman_RON_from_ignition_delay(ignition_delay): r'''Esimates the research octane number (RON) from a known ignition delay, as shown in [1]_. .. math:: \text{RON} = 120.77 - \frac{425.48}{\tau_{ID}} In the above equation, ignition delay is in ms. Parameters ---------- ignition_delay : float The ignition delay, [s] Returns ------- RON : float Research Octane Number [-] Notes ----- The correlation was developed using 20 components, for a range of approximately 3.6 ms to 67 ms. Examples -------- >>> Perez_Boehman_RON_from_ignition_delay(1/150) 56.948 References ---------- .. [1] Perez, Peter L., and André L. Boehman. "Experimental Investigation of the Autoignition Behavior of Surrogate Gasoline Fuels in a Constant-Volume Combustion Bomb Apparatus and Its Relevance to HCCI Combustion." Energy & Fuels 26, no. 10 (October 18, 2012): 6106-17. https://doi.org/10.1021/ef300503b. ''' ignition_delay *= 1e3 # convert from seconds to ms return 120.77 - 425.48/ignition_delay
[docs]def Perez_Boehman_MON_from_ignition_delay(ignition_delay): r'''Esimates the motor octane number (MON) from a known ignition delay, as shown in [1]_. .. math:: \text{MON} = 109.93 - \frac{374.73}{\tau_{ID}} In the above equation, ignition delay is in ms. Parameters ---------- ignition_delay : float The ignition delay, [s] Returns ------- MON : float Motor Octane Number [-] Notes ----- The correlation was developed using 20 components, for a range of approximately 3.6 ms to 67 ms. Examples -------- >>> Perez_Boehman_MON_from_ignition_delay(1/150) 53.7205 References ---------- .. [1] Perez, Peter L., and André L. Boehman. "Experimental Investigation of the Autoignition Behavior of Surrogate Gasoline Fuels in a Constant-Volume Combustion Bomb Apparatus and Its Relevance to HCCI Combustion." Energy & Fuels 26, no. 10 (October 18, 2012): 6106-17. https://doi.org/10.1021/ef300503b. ''' ignition_delay *= 1e3 # convert from seconds to ms return 109.93 - 374.73/ignition_delay
[docs]def IDT_to_DCN(IDT): r'''This function converts the ignition delay time [1]_ into a derived cetane number. If the ignition delay time is between 3.1 and 6.5 ms: .. math:: \text{DCN} = 4.46 + \frac{186.6}{\text{IDT}} Otherwise: .. math:: \text{DCN} = \left(83.99(\text{IDT} - 1.512)^{-0.658} \right) + 3.547 Parameters ---------- IDT : float Ignition delay time, [s] Returns ------- DCN : float Derived cetane number, [-] Notes ----- This conversion is described in D6890-168. Examples -------- >>> IDT_to_DCN(4e-3) 51.11 References ---------- .. [1] Al Ibrahim, Emad, and Aamir Farooq. "Prediction of the Derived Cetane Number and Carbon/Hydrogen Ratio from Infrared Spectroscopic Data." Energy & Fuels 35, no. 9 (May 6, 2021): 8141-52. https://doi.org/10.1021/acs.energyfuels.0c03899. .. [2] Dahmen, Manuel, and Wolfgang Marquardt. "A Novel Group Contribution Method for the Prediction of the Derived Cetane Number of Oxygenated Hydrocarbons." Energy & Fuels 29, no. 9 (September 17, 2015): 5781-5801. https://doi.org/10.1021/acs.energyfuels.5b01032. ''' IDT *= 1000.0 if 3.1 < IDT < 6.5: return 4.46 + (186.6/IDT) else: return (83.99*(IDT-1.512)**-0.658) + 3.547
@mark_numba_incompatible def as_atoms(formula): if isinstance(formula, str): atoms = simple_formula_parser(formula) elif isinstance(formula, dict): atoms = formula else: raise ValueError("atoms must be either a string or dictionary, " "not a %s object" %(type(formula).__name__)) return atoms DULONG = 'Dulong' STOICHIOMETRY = 'Stoichiometry' HHV_methods = (DULONG, STOICHIOMETRY) combustible_elements = ('C', 'H', 'N', 'O', 'S', 'Br', 'I', 'Cl', 'F', 'P') combustible_elements_set = frozenset(combustible_elements) Hf_combustion_chemicals = { 'H2O': -285825.0, 'CO2': -393474.0, 'SO2': -296800.0, 'Br2': 30880.0, 'I2': 62417.0, 'HCl': -92173.0, 'HF': -272711.0, 'P4O10': -3009940.0, 'O2': 0.0, 'N2': 0.0, 'Ar': 0.0, 'Ne': 0.0, 'Kr': 0.0, 'Xe': 0.0, 'He': 0.0, "Ash": 0.0, } incombustible_materials = { '7440-37-1': 'Ar', '7440-59-7': 'He', '7782-44-7': 'O2', '7440-01-9': 'Ne', '7439-90-9': 'Kr', '7440-63-3': 'Xe', '7727-37-9': 'N2', '124-38-9': 'CO2', '1314-13-2': 'ZnO2', '7732-18-5': 'water', '7789-20-0': 'water(D2)', '13463-67-7': 'TiO2', '14762-55-1': 'He3', '7782-50-5': 'Cl', '7446-09-5': 'SO2', '7726-95-6': 'Br'} combustion_products_to_CASs = { 'Br2': '7726-95-6', 'CO2': '124-38-9', 'H2O': '7732-18-5', 'HCl': '7647-01-0', 'HF': '7664-39-3', 'I2': '7553-56-2', 'N2': '7727-37-9', 'O2': '7782-44-7', 'Ar': '7440-37-1', 'He': '7440-59-7', 'P4O10': '16752-60-6', 'SO2': '7446-09-5'} unreactive_CASs = { '124-38-9': 'CO2', '16752-60-6': 'P4O10', '7446-09-5': 'SO2', '7553-56-2': 'I2', '7647-01-0': 'HCl', '7664-39-3': 'HF', '7726-95-6': 'Br2', '7727-37-9': 'N2', '7732-18-5': 'H2O', '7440-37-1': 'Ar', '7440-59-7': 'He'} O2_CAS = '7782-44-7' H2O_CAS = '7732-18-5'
[docs]@mark_numba_incompatible def combustion_stoichiometry(atoms, MW=None, missing_handling='elemental'): r"""Return a dictionary of stoichiometric coefficients of chemical combustion, given a dictionary of a molecule's constituent atoms and their counts. This function is based on the combustion of hydrocarbons; the products for some inorganics can be hard to predict, and no special handling is included here for them. This reaction is the standard one at standard pressure with an excess of oxygen; it does not account for partial combustion or nitrous oxides. Parameters ---------- atoms : dict[str, int] Dictionary of atoms and their counts, [-] MW : float, optional Molecular weight of chemical, used only if `missing_handling` is 'Ash', [g/mol] missing_handling : str, optional How to handle compounds which do not appear in the stoichiometric reaction below. If 'elemental', return those atoms in the monatomic state; if 'ash', converts all missing attoms to 'Ash' in the output at a `MW` of 1 g/mol, [-] Returns ------- stoichiometry : dict[str, float] Stoichiometric coefficients of combustion. May inlcude the following keys for complete combustion: 'H2O', 'CO2', 'SO2', 'Br2', 'I2', 'HCl', 'HF' 'P4O10'; if `missing_handling` is 'elemental' can include the other elements; if `missing_handling` is 'ash', Ash will be present in the output if the compounds whose reactions are not included here. 'O2' is always present, with negative values indicating oxygen is required. [-] Notes ----- The stoichiometry is given by: .. math:: C_c H_h O_o N_n S_s Br_b I_i Cl_x F_f P_p + kO_2 -> cCO_2 + \frac{b}{2}Br_2 + \frac{i}{2}I + xHCl + fHF + sSO_2 + \frac{n}{2}N_2 + \frac{p}{4}P_4O_{10} +\frac{h + x + f}{2}H_2O .. math:: k = c + s + \frac{h}{4} + \frac{5P}{4} - \frac{x + f}{4} - \frac{o}{2} Also included in the results is the moles of O2 required per mole of the mixture of the molecule. HF and HCl are gaseous products in their standard state. P4O10 is a solid in its standard state. Bromine is a liquid as is iodine. Water depends on the chosen definition of heating value. The other products are gases. Atoms not in ['C', 'H', 'N', 'O', 'S', 'Br', 'I', 'Cl', 'F', 'P'] are returned as pure species; i.e. sodium hydroxide produces water and pure Na. Examples -------- Methane gas burning: >>> combustion_stoichiometry({'C': 1, 'H':4}) {'CO2': 1, 'O2': -2.0, 'H2O': 2.0} """ products = {} nC, nH, nN, nO, nS, nBr, nI, nCl, nF, nP = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 if 'C' in atoms: products['CO2'] = nC = atoms['C'] if 'H' in atoms: nH = atoms['H'] if 'N' in atoms: nN = atoms['N'] if 'O' in atoms: nO = atoms['O'] if 'S' in atoms: nS = atoms['S'] if 'Br' in atoms: nBr = atoms['Br'] if 'I' in atoms: nI = atoms['I'] if 'Cl' in atoms: nCl = atoms['Cl'] if 'F' in atoms: nF = atoms['F'] if 'P' in atoms: nP = atoms['P'] nO2 = -(nC + nS + .25*nH + 1.25*nP - .25*(nCl + nF) - .5*nO) nCO2 = nC nBr2 = .5*nBr nI2 = .5*nI nHCl = nCl nHF = nF nSO2 = nS nN2 = .5*nN nP4O10 = .25*nP nH2O = 0.5*(nH - nCl - nF) if nO2 != 0.0: products['O2'] = nO2 if nCO2 != 0.0: products['CO2'] = nCO2 if nBr2 != 0.0: products['Br2'] = nBr2 if nI2 != 0.0: products['I2'] = nI2 if nCl != 0.0: products['HCl'] = nHCl if nHF != 0.0: products['HF'] = nHF if nSO2 != 0.0: products['SO2'] = nSO2 if nN2 != 0.0: products['N2'] = nN2 if nP4O10 != 0.0: products['P4O10'] = nP4O10 if nH2O != 0.0: products['H2O'] = nH2O missing_handling = missing_handling.lower() if missing_handling == 'elemental': for atom, value in atoms.items(): if atom not in combustible_elements_set: products[atom] = value elif missing_handling == 'ash': combustion_atoms = {i: atoms.get(i, 0) for i in combustible_elements} MW = MW or molecular_weight(atoms) Ash = MW - molecular_weight(combustion_atoms) if Ash/MW > 0.0001: products['Ash'] = Ash else: raise ValueError("Allowed values for `missing_handling` are 'elemental' and 'ash'.") return products
[docs]@mark_numba_incompatible def combustion_products_mixture(atoms_list, zs, reactivities=None, CASs=None, missing_handling='elemental', combustion_stoichiometries=None): """Calculates the combustion products of a mixture of molecules and their, mole fractions; requires a list of dictionaries of each molecule's constituent atoms and their counts. Products for non-hydrocarbons may not be correct, but are still calculated. Parameters ---------- atoms_list : list[dict] List of dictionaries of atoms and their counts, [-] zs : list[float] Mole fractions of each molecule in the mixture, [-] reactivities : list[bool] Indicators as to whether to combust each molecule, [-] CASs : list[str] CAS numbers of all compounds; non-reacted products will appear in the products indexed by their CAS number, [-] missing_handling : str, optional How to handle compounds which do not appear in the stoichiometric reaction below. If 'elemental', return those atoms in the monatomic state; if 'Ash', converts all missing attoms to 'Ash' in the output at a `MW` of 1 g/mol, [-] combustion_stoichiometries : list[dict[str, float]] List of return values from `combustion_stoichiometry`, can be provided if precomputed [-] Returns ------- combustion_producucts : dict Dictionary of combustion products and their counts, [-] Notes ----- Also included in the results is the moles of O2 required per mole of the mixture to be burnt. Note that if O2 is in the feed, this will be subtracted from the required O2 amount. HF and HCl are gaseous products in their standard state. P4O10 is a solid in its standard state. Bromine is a liquid as is iodine. Water depends on the chosen definition of heating value. The other products are gases. Note that if instead of mole fractions, mole flows are given - the results are in terms of mole flows as well! Examples -------- Mixture of methane and ethane. >>> combustion_products_mixture([{'H': 4, 'C': 1}, {'H': 6, 'C': 2}, {'Ar': 1}, {'C': 15, 'H': 32}], ... [.9, .05, .04, .01], reactivities=[True, True, True, False], ... CASs=['74-82-8', '74-84-0', '7440-37-1', '629-62-9']) {'CO2': 1.0, 'O2': -1.975, 'H2O': 1.9500000000000002, 'Ar': 0.04, '629-62-9': 0.01} """ # Attempted to use a .copy() on a base dict but that was slower products = {} has_reactivities = reactivities is not None missing_combustion_stoichiometries = not combustion_stoichiometries for i, (atoms, zs_i) in enumerate(zip(atoms_list, zs)): if has_reactivities and not reactivities[i]: products[CASs[i]] = zs_i else: ans = (combustion_stoichiometry(atoms, missing_handling=missing_handling) if missing_combustion_stoichiometries else combustion_stoichiometries[i]) for key, val in ans.items(): if key in products: delta = val*zs_i if delta != 0.0 and (abs((delta + products[key])/delta) < 4e-15): products[key] = 0.0 else: products[key] += delta else: products[key] = val*zs_i return products
def combustion_products_to_list(products, CASs): zs = [0.0 for i in CASs] for product, zi in products.items(): if product == 'O2_required': product = 'O2' zi = -zi if product in CASs: zs[CASs.index(product)] = zi elif combustion_products_to_CASs[product] in CASs: zs[CASs.index(combustion_products_to_CASs[product])] = zi else: if abs(zi) > 0: raise ValueError("Combustion product not in package") return zs @mark_numba_incompatible def is_combustible(CAS, atoms, reactive=True): if not reactive: return False if CAS in unreactive_CASs: return False elif 'C' in atoms and atoms['C'] > 0.0: return True return bool('H' in atoms and atoms['H'] > 0.0)
[docs]@mark_numba_incompatible def HHV_stoichiometry(stoichiometry, Hf, Hf_chemicals=None): r""" Return the higher heating value [HHV; in J/mol] based on the theoretical combustion stoichiometry and the heat of formation of the chemical. Parameters ---------- stoichiometry : dict[str, float] Stoichiometric coefficients of combustion. May inlcude the following keys: 'H2O', 'CO2', 'SO2', 'Br2', 'I2', 'HCl', 'HF' and 'P4O10'. Hf : float Heat of formation [J/mol]. Hf_chemicals : dict[str, float] Heat of formation of chemicals present in stoichiometry, [J/mol] Returns ------- HHV : float Higher heating value [J/mol]. Notes ----- The combustion reaction is based on the following equation: .. math:: C_c H_h O_o N_n S_s Br_b I_i Cl_x F_f P_p + kO_2 -> cCO_2 + \frac{b}{2}Br_2 + \frac{i}{2}I + xHCl + fHF + sSO_2 + \frac{n}{2}N_2 + \frac{p}{4}P_4O_{10} +\frac{h + x + f}{2}H_2O .. math:: k = c + s + \frac{h}{4} + \frac{5P}{4} - \frac{x + f}{4} - \frac{o}{2} The HHV is calculated as the heat of reaction. Examples -------- Burning methane gas: >>> HHV_stoichiometry({'O2': -2.0, 'CO2': 1, 'H2O': 2.0}, -74520.0) -890604.0 """ Hfs = Hf_chemicals or Hf_combustion_chemicals return sum([Hfs[i] * j for i, j in stoichiometry.items()]) - Hf
[docs]@mark_numba_incompatible def HHV_modified_Dulong(mass_fractions): r""" Return higher heating value [HHV; in J/g] based on the modified Dulong's equation [1]_. Parameters ---------- mass_fractions : dict[str, float] Dictionary of atomic mass fractions [-]. Returns ------- HHV : float Higher heating value [J/mol]. Notes ----- The heat of combustion in J/mol is given by Dulong's equation [1]_: .. math:: Hc (J/mol) = MW \cdot (338C + 1428(H - O/8)+ 95S) This equation is only good for <10 wt. % Oxygen content. Variables C, H, O, and S are atom weight fractions. Examples -------- Dry bituminous coal: >>> HHV_modified_Dulong({'C': 0.716, 'H': 0.054, 'S': 0.016, 'N': 0.016, 'O': 0.093, 'Ash': 0.105}) -304.0395 References ---------- .. [1] Green, D. W. Waste management. In Perry`s Chemical Engineers` Handbook, 9 ed.; McGraw-Hill Education, 2018 """ C = mass_fractions.get('C', 0.) H = mass_fractions.get('H', 0.) O = mass_fractions.get('O', 0.) S = mass_fractions.get('S', 0.) if O > 0.105: raise ValueError("Dulong's formula is only valid at 10 wt. %% Oxygen " "or less (%s given)" %(O)) return - (338.*C + 1428.*(H - O/8.)+ 95.*S)
[docs]def LHV_from_HHV(HHV, N_H2O): r""" Return the lower heating value [LHV; in J/mol] of a chemical given the higher heating value [HHV; in J/mol] and the number of water molecules formed per molecule burned. Parameters ---------- HHV : float Higher heating value [J/mol]. N_H2O : int Number of water molecules produced [-]. Returns ------- LHV : float Lower heating value [J/mol]. Notes ----- The LHV is calculated as follows: .. math:: LHV = HHV + H_{vap} \cdot H_2O .. math:: H_{vap} = 44011.496 \frac{J}{mol H_2O} .. math:: H_2O = \frac{mol H_2O}{mol} Examples -------- Methanol lower heat of combustion: >>> LHV_from_HHV(-726024.0, 2) -638001.008 """ return HHV + 44011.496 * N_H2O
[docs]@mark_numba_incompatible def combustion_data(formula=None, stoichiometry=None, Hf=None, MW=None, method=None, missing_handling='ash'): r""" Return a CombustionData object (a named tuple) that contains the stoichiometry coefficients of the reactants and products, the lower and higher heating values [LHV, HHV; in J/mol], the heat of formation [Hf; in J/mol], and the molecular weight [MW; in g/mol]. Parameters ---------- formula : str, or dict[str, float], optional Chemical formula as a string or a dictionary of atoms and their counts. stoichiometry : dict[str, float], optional Stoichiometry of combustion reaction. Hf : float, optional Heat of formation of given chemical [J/mol]. Required if method is "Stoichiometry". MW : float, optional Molecular weight of chemical [g/mol]. method : "Stoichiometry" or "Dulong", optional Method to estimate LHV and HHV. missing_handling : str, optional How to handle compounds which do not appear in the stoichiometric reaction below. If 'elemental', return those atoms in the monatomic state; if 'Ash', converts all missing attoms to 'Ash' in the output at a `MW` of 1 g/mol, [-] Returns ------- combustion_data : :class:`~chemicals.combustion.CombustionData` A combustion data object with the stoichiometric coefficients of combustion, higher heating value, heat of formation, and molecular weight as attributes named stoichiomery, HHV, Hf, and MW, respectively. Notes ----- The combustion reaction is based on the following equation: .. math:: C_c H_h O_o N_n S_s Br_b I_i Cl_x F_f P_p + kO_2 -> cCO_2 + \frac{b}{2}Br_2 + \frac{i}{2}I + xHCl + fHF + sSO_2 + \frac{n}{2}N_2 + \frac{p}{4}P_4O_{10} +\frac{h + x + f}{2}H_2O .. math:: k = c + s + \frac{h}{4} + \frac{5P}{4} - \frac{x + f}{4} - \frac{o}{2} If the method is "Stoichiometry", the HHV is found using through an energy balance on the reaction (i.e. heat of reaction). If the method is "Dulong", Dulong's equation is used [1]_: .. math:: Hc (J/mol) = MW \cdot (338C + 1428(H - O/8)+ 95S) The LHV is calculated as follows: .. math:: LHV = HHV + H_{vap} \cdot H_2O .. math:: H_{vap} = 44011.496 \frac{J}{mol H_2O} .. math:: H_2O = \frac{mol H_2O}{mol} Examples -------- Liquid methanol burning: >>> combustion_data({'H': 4, 'C': 1, 'O': 1}, Hf=-239100) CombustionData(stoichiometry={'CO2': 1, 'O2': -1.5, 'H2O': 2.0}, HHV=-726024.0, Hf=-239100, MW=32.04186) References ---------- .. [1] Green, D. W. Waste management. In Perry`s Chemical Engineers` Handbook, 9 ed.; McGraw-Hill Education, 2018 """ if formula: if stoichiometry: raise ValueError("must specify either `formula` or `stoichiometry`, not both") atoms = as_atoms(formula) if not stoichiometry: try: stoichiometry = combustion_stoichiometry(atoms, MW, missing_handling) except NameError: raise ValueError("must specify either `formula` or `stoichiometry`, none specified") if MW is None: MW = molecular_weight(atoms) if method: method = method.capitalize() else: method = 'Dulong' if Hf is None else 'Stoichiometry' if method == DULONG: HHV = MW * HHV_modified_Dulong(mass_fractions(atoms)) if Hf: raise ValueError("cannot specify Hf if method is 'Dulong'") Hf = HHV - HHV_stoichiometry(stoichiometry, 0) elif method == STOICHIOMETRY: if Hf is None: raise ValueError("must specify Hf if method is 'Stoichiometry'") HHV = HHV_stoichiometry(stoichiometry, Hf) else: raise ValueError("method must be either 'Stoichiometric' or 'Dulong', " "not %s" %(method)) return CombustionData(stoichiometry, HHV, Hf, MW)
[docs]class CombustionData: r""" Return a CombustionData object (a named tuple) that contains the stoichiometry coefficients of the reactants and products, the lower and higher heating values [LHV, HHV; in J/mol], the heat of formation [Hf; in J/mol], and the molecular weight [MW; in g/mol]. Parameters ---------- stoichiometry : dict[str, float] Stoichiometric coefficients of the reactants and products. HHV : float Higher heating value [J/mol]. Hf : float Heat of formation [J/mol]. MW : float Molecular weight [g/mol]. """ def __init__(self, stoichiometry, HHV, Hf, MW): self.stoichiometry = stoichiometry self.HHV = HHV self.Hf = Hf self.MW = MW @property def LHV(self): """Lower heating value [LHV; in J/mol]""" return LHV_from_HHV(self.HHV, self.stoichiometry.get('H2O', 0.)) def __repr__(self): return 'CombustionData(stoichiometry={}, HHV={}, Hf={}, MW={})'.format( self.stoichiometry, self.HHV, self.Hf, self.MW)
[docs]def air_fuel_ratio_solver(ratio, Vm_air, Vm_fuel, MW_air, MW_fuel, n_air=None, n_fuel=None, basis='mass'): """Calculates molar flow rate of air or fuel from the other, using a specified air-fuel ratio. Supports 'mole', 'mass', and 'volume'. bases for the ratio variable. The ratio must be of the same units - i.e. kg/kg instead of lb/kg. The mole, mass, and volume air-fuel ratios are calculated in the process and returned as well. Parameters ---------- ratio : float Air-fuel ratio, in the specified `basis`, [-] Vm_air : float Molar volume of air, [m^3/mol] Vm_fuel : float Molar volume of fuel, [m^3/mol] MW_air : float Molecular weight of air, [g/mol] MW_fuel : float Molecular weight of fuel, [g/mol] n_air : float, optional Molar flow rate of air, [mol/s] n_fuel : float, optional Molar flow rate of fuel, [mol/s] basis : str, optional One of 'mass', 'mole', or 'volume', [-] Returns ------- n_air : float Molar flow rate of air, [mol/s] n_fuel : float Molar flow rate of fuel, [mol/s] mole_ratio : float Air-fuel mole ratio, [-] mass_ratio : float Air-fuel mass ratio, [-] volume_ratio : float Air-fuel volume ratio, [-] Notes ----- The function works so long as the flow rates, molar volumes, and molecular weights are in a consistent basis. The function may also be used to obtain the other ratios, even if both flow rates are known. Be careful to use standard volumes if the ratio known is at standard conditions! This function has no provision for mixed units like mass/mole or volume/mass. Examples -------- >>> Vm_air = 0.024936627188566596 >>> Vm_fuel = 0.024880983160354486 >>> MW_air = 28.850334 >>> MW_fuel = 17.86651 >>> n_fuel = 5.0 >>> n_air = 25.0 >>> air_fuel_ratio_solver(ratio=5.0, Vm_air=Vm_air, Vm_fuel=Vm_fuel, ... MW_air=MW_air, MW_fuel=MW_fuel, n_air=n_air, ... n_fuel=n_fuel, basis='mole') (25.0, 5.0, 5.0, 8.073858296891782, 5.011182039683378) """ if basis == 'mole': if n_air is not None and n_fuel is None: n_fuel = n_air/ratio elif n_fuel is not None and n_air is None: n_air = n_fuel*ratio elif basis == 'mass': if n_air is not None and n_fuel is None: m_air = property_mass_to_molar(n_air, MW_air) m_fuel = m_air/ratio n_fuel = property_molar_to_mass(m_fuel, MW_fuel) elif n_fuel is not None and n_air is None: m_fuel = property_mass_to_molar(n_fuel, MW_fuel) m_air = m_fuel*ratio n_air = property_molar_to_mass(m_air, MW_air) elif basis == 'volume': if n_air is not None and n_fuel is None: V_air = n_air*Vm_air V_fuel = V_air/ratio n_fuel = V_fuel/Vm_fuel elif n_fuel is not None and n_air is None: V_fuel = n_fuel*Vm_fuel V_air = V_fuel*ratio n_air = V_air/Vm_air if n_air is None or n_fuel is None: raise ValueError("Could not convert") mole_ratio = n_air/n_fuel mass_ratio, volume_ratio = MW_air/MW_fuel*mole_ratio, Vm_air/Vm_fuel*mole_ratio return n_air, n_fuel, mole_ratio, mass_ratio, volume_ratio
[docs]@mark_numba_incompatible def fuel_air_spec_solver(zs_air, zs_fuel, CASs, atomss, n_fuel=None, n_air=None, n_out=None, O2_excess=None, frac_out_O2=None, frac_out_O2_dry=None, ratio=None, Vm_air=None, Vm_fuel=None, MW_air=None, MW_fuel=None, ratio_basis='mass', reactivities=None, combustion_stoichiometries=None): """Solves the system of equations describing a flow of air mixing with a flow of combustibles and burning completely. All calculated variables are returned as a dictionary. Supports solving with any 2 of the extensive variables, or one extensive and one intensive variable: Extensive variables: * `n_air` * `n_fuel` * `n_out` Intensive variables: * `O2_excess` * `frac_out_O2` * `frac_out_O2_dry` * `ratio` The variables `Vm_air`, `Vm_fuel`, `MW_air`, and `MW_fuel` are only required when an air-fuel ratio is given. Howver, the ratios cannot be calculated for the other solve options without them. Parameters ---------- zs_air : list[float] Mole fractions of the air; most not contain any combustibles, [-] zs_fuel : list[float] Mole fractions of the fuel; can contain inerts and/or oxygen as well, [-] CASs : list[str] CAS numbers of all compounds, [-] atomss : list[dict[float]] List of dictionaries of elements and their counts for all molecules in the mixtures, [-] n_fuel : float, optional Flow rate of fuel, [mol/s] n_air : float, optional Flow rate of air, [mol/s] n_out : float, optional Flow rate of combustion products, remaining oxygen, and inerts, [mol/s] O2_excess : float, optional The excess oxygen coming out; (O2 in)/(O2 required) - 1, [-] frac_out_O2 : float, optional The mole fraction of oxygen out, [-] frac_out_O2_dry : float, optional The mole fraction of oxygen out on a dry basis, [-] ratio : float, optional Air-fuel ratio, in the specified `basis`, [-] Vm_air : float, optional Molar volume of air, [m^3/mol] Vm_fuel : float, optional Molar volume of fuel, [m^3/mol] MW_air : float, optional Molecular weight of air, [g/mol] MW_fuel : float, optional Molecular weight of fuel, [g/mol] ratio_basis : str, optional One of 'mass', 'mole', or 'volume', [-] reactivities : list[bool], optional Optional list which can be used to mark otherwise combustible compounds as incombustible and which will leave unreacted, [-] combustion_stoichiometries : list[dict[str, float]] List of return values from `combustion_stoichiometry`, can be provided if precomputed [-] Returns ------- results : dict * n_fuel : Flow rate of fuel, [mol/s] * n_air : Flow rate of air, [mol/s] * n_out : Flow rate of combustion products, remaining oxygen, and inerts, [mol/s] * O2_excess : The excess oxygen coming out; (O2 in)/(O2 required) - 1, [-] * frac_out_O2 : The mole fraction of oxygen out, [-] * frac_out_O2_dry : The mole fraction of oxygen out on a dry basis, [-] * mole_ratio : Air-fuel mole ratio, [-] * mass_ratio : Air-fuel mass ratio, [-] * volume_ratio : Air-fuel volume ratio, [-] * ns_out : Mole flow rates out, [mol/s] * zs_out : Mole fractions out, [-] Notes ----- Combustion products themselves cannot be set as unreactive. The function works so long as the flow rates, molar volumes, and molecular weights are in a consistent basis. The function may also be used to obtain the other ratios, even if both flow rates are known. Be careful to use standard volumes if the ratio known is at standard conditions! Examples -------- >>> zs_air = [0.79, 0.205, 0, 0, 0, 0.0045, 0.0005] >>> zs_fuel = [0.025, 0.025, 0.85, 0.07, 0.029, 0.0005, 0.0005] >>> CASs = ['7727-37-9', '7782-44-7', '74-82-8', '74-84-0', '74-98-6', '7732-18-5', '124-38-9'] >>> atomss = [{'N': 2}, {'O': 2}, {'H': 4, 'C': 1}, {'H': 6, 'C': 2}, {'H': 8, 'C': 3}, {'H': 2, 'O': 1}, {'C': 1, 'O': 2}] >>> ans = fuel_air_spec_solver(zs_air=zs_air, zs_fuel=zs_fuel, CASs=CASs, atomss=atomss, n_fuel=5.0, O2_excess=0.3, Vm_air=0.02493, Vm_fuel=0.02488, MW_air=28.79341351, MW_fuel=18.55158039) >>> [round(i, 5) for i in ans['ns_out']] [51.99524, 3.135, 0.0, 0.0, 0.0, 10.42796, 5.42033] >>> [round(i, 5) for i in ans['zs_out']] [0.73255, 0.04417, 0.0, 0.0, 0.0, 0.14692, 0.07637] >>> ans['frac_out_O2'], ans['frac_out_O2_dry'] (0.04416828172034148, 0.051774902132807) >>> ans['mole_ratio'], ans['mass_ratio'], ans['volume_ratio'] (13.131707317073175, 20.381372957130615, 13.15809740412517) >>> ans['n_air'] 65.65853658536588 """ TRACE_FRACTION_IN_AIR = 1e-10 # Only one path to get n_air, n_fuel should ever be followed. Calculate all the # extra information redundantly at the end! # Handle combustibles in the air by burning them right away, # and working with that n_air. N = len(CASs) cmps = range(N) if reactivities is None: reactivities = [True]*len(zs_air) combustibilities = [is_combustible(CASs[i], atomss[i], reactivities[i]) for i in cmps] O2_index = CASs.index(O2_CAS) H2O_index = CASs.index(H2O_CAS) z_air_O2 = zs_air[O2_index] z_air_H20 = zs_air[H2O_index] z_fuel_O2 = zs_fuel[O2_index] if ratio is not None and (n_air is None or n_fuel is None): n_air, n_fuel = air_fuel_ratio_solver(ratio, Vm_air, Vm_fuel, MW_air, MW_fuel, n_air=n_air, n_fuel=n_fuel, basis=ratio_basis)[0:2] # Given O2 excess and either air or fuel flow rate, can solve directly for the other if O2_excess is not None and (n_fuel is None or n_air is None): if n_fuel is not None: comb_ans = combustion_products_mixture(atomss, zs_fuel, reactivities=reactivities, CASs=CASs, combustion_stoichiometries=combustion_stoichiometries) n_O2_required = -comb_ans['O2']*n_fuel + z_fuel_O2*n_fuel N_O2_required_air = n_O2_required*(1.0 + O2_excess)- z_fuel_O2*n_fuel n_air = N_O2_required_air/zs_air[O2_index] elif n_air is not None: """from sympy import * O2, n_fuel, O2_per_mole_fuel, n_O2_air, z_fuel_O2 = symbols('O2, n_fuel, O2_per_mole_fuel, n_O2_air, z_fuel_O2') Eq1 = Eq(O2/n_fuel, O2_per_mole_fuel) Eq2 = Eq(O2, n_O2_air + z_fuel_O2*n_fuel) solve([Eq1, Eq2], [n_fuel, O2])""" comb_ans = combustion_products_mixture(atomss, zs_fuel, reactivities=reactivities, CASs=CASs, combustion_stoichiometries=combustion_stoichiometries) O2_per_mole_fuel = (-comb_ans['O2'] + zs_fuel[O2_index])*(1.0 + O2_excess) n_O2_air = zs_air[O2_index]*n_air n_fuel = n_O2_air/(O2_per_mole_fuel - z_fuel_O2) elif n_out is not None: """from sympy import * n_air, n_fuel, n_out, n_delta, O2_coeff, O2_excess, z_air_O2, z_fuel_O2 = symbols('n_air, n_fuel, n_out, n_delta, O2_coeff, O2_excess, z_air_O2, z_fuel_O2') n_O2_in = n_air*z_air_O2 + z_fuel_O2*n_fuel Eq1 = Eq(O2_excess, n_O2_in/(n_fuel*O2_coeff) - 1) Eq2 = Eq(n_out, n_air + (n_delta)*n_fuel + n_fuel) solve([Eq1, Eq2], [n_fuel, n_air])""" comb_ans = combustion_products_mixture(atomss, zs_fuel, reactivities=reactivities, CASs=CASs, combustion_stoichiometries=combustion_stoichiometries) stoic_comb_products = combustion_products_to_list(comb_ans, CASs) O2_coeff = O2_burnt_n_fuel = -comb_ans['O2'] + z_fuel_O2 n_delta = 1 for z_new, z_old, CAS in zip(stoic_comb_products, zs_fuel, CASs): if CAS != O2_CAS: n_delta += z_new - z_old n_delta -= -comb_ans['O2'] + z_fuel_O2 n_fuel = n_out*z_air_O2/(O2_coeff*O2_excess + O2_coeff + n_delta*z_air_O2 - z_fuel_O2) n_air = n_out*(O2_coeff*O2_excess + O2_coeff - z_fuel_O2)/(O2_coeff*O2_excess + O2_coeff + n_delta*z_air_O2 - z_fuel_O2) # n_fuel = n_out*z_air_O2/(O2_burnt_n_fuel*O2_excess + O2_burnt_n_fuel + n_delta*z_air_O2) # n_air = O2_burnt_n_fuel*n_out*(O2_excess + 1)/(O2_burnt_n_fuel*O2_excess + O2_burnt_n_fuel + n_delta*z_air_O2) # Solvers for frac_out_O2 and dry basis if (frac_out_O2 is not None or frac_out_O2_dry is not None) and (n_fuel is None or n_air is None): if n_fuel is not None: ns_fuel = [zi*n_fuel for zi in zs_fuel] comb_ans = combustion_products_mixture(atomss, ns_fuel, reactivities=reactivities, CASs=CASs, combustion_stoichiometries=combustion_stoichiometries) n_O2_stoic = -comb_ans['O2'] if n_O2_stoic < 0.0: raise ValueError("Cannot meet air spec - insufficient air for full combustion") # when burning stoichiometrically, how many moles out? stoic_comb_products = combustion_products_to_list(comb_ans, CASs) stoic_comb_products[O2_index] = 0 # Set the O2 to zero as it is negative n_fixed_products = sum(stoic_comb_products) # Following two equations solved using SymPy if frac_out_O2 is not None: """from sympy import * from sympy.abc import * frac_goal, n_air, n_fixed, z_O2, O2_burnt = symbols('frac_goal, n_air, n_fixed, z_O2, O2_burnt') solve(Eq(frac_goal, (z_O2*n_air-O2_burnt)/((n_fixed+n_air)-O2_burnt)), n_air) """ n_air = (n_O2_stoic*frac_out_O2 - n_O2_stoic - frac_out_O2*n_fixed_products)/(frac_out_O2 - z_air_O2) elif frac_out_O2_dry is not None: """from sympy import * from sympy.abc import * frac_goal, n_air, n_fixed, z_O2, n_O2_stoic, z_H20 = symbols('frac_goal, n_air, n_fixed, z_O2, n_O2_stoic, z_H20') solve(Eq(frac_goal, (z_O2*n_air-n_O2_stoic)/((n_fixed+n_air)-n_O2_stoic -n_air*z_H20)), n_air) """ n_fixed_products -= stoic_comb_products[H2O_index] n_air = ((-frac_out_O2_dry*n_O2_stoic + frac_out_O2_dry*n_fixed_products + n_O2_stoic) /(frac_out_O2_dry*z_air_H20 - frac_out_O2_dry + z_air_O2)) elif n_air is not None or n_out is not None: comb_ans = combustion_products_mixture(atomss, zs_fuel, reactivities=reactivities, CASs=CASs, combustion_stoichiometries=combustion_stoichiometries) O2_burnt_n_fuel = -comb_ans['O2'] H2O_from_fuel_n = comb_ans['H2O'] n_delta = 0.0 for k, v in comb_ans.items(): n_delta += v if frac_out_O2 is not None and n_air is not None: """from sympy import * from sympy.abc import * frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff = symbols('frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff') n_unburnt_O2 = n_air*z_O2 - n_fuel*O2_coeff n_out = n_air + coeff*n_fuel solve(Eq(frac_goal, n_unburnt_O2/n_out), n_fuel) """ n_fuel = n_air*(-frac_out_O2 + z_air_O2)/(O2_burnt_n_fuel + n_delta*frac_out_O2) elif frac_out_O2_dry is not None and n_air is not None: """from sympy import * from sympy.abc import * frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff, H2O_coeff, z_H2O = symbols('frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff, H2O_coeff, z_H2O') n_unburnt_O2 = n_air*z_O2 - n_fuel*O2_coeff n_out = n_air + coeff*n_fuel n_H2O = n_air*z_H2O + H2O_coeff*n_fuel solve(Eq(frac_goal, n_unburnt_O2/(n_out-n_H2O)), n_fuel)""" n_fuel = n_air*(frac_out_O2_dry*z_air_H20 - frac_out_O2_dry + z_air_O2)/(-H2O_from_fuel_n*frac_out_O2_dry + O2_burnt_n_fuel + n_delta*frac_out_O2_dry) elif frac_out_O2 is not None and n_out is not None: """from sympy import * frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff, n_out = symbols('frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff, n_out') n_unburnt_O2 = n_air*z_O2 - n_fuel*O2_coeff Eq1 = Eq(frac_goal, n_unburnt_O2/n_out) Eq2 = Eq(n_out, n_air + coeff*n_fuel) solve([Eq1, Eq2], [n_fuel, n_air])""" n_fuel = -n_out*(frac_out_O2 - z_air_O2)/(O2_burnt_n_fuel + n_delta*z_air_O2) n_air = n_out*(O2_burnt_n_fuel + n_delta*frac_out_O2)/(O2_burnt_n_fuel + n_delta*z_air_O2) elif frac_out_O2_dry is not None and n_out is not None: """from sympy import * frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff, n_out, z_H2O, H2O_coeff = symbols('frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff, n_out, z_H2O, H2O_coeff') n_unburnt_O2 = n_air*z_O2 - n_fuel*O2_coeff n_H2O = n_air*z_H2O + H2O_coeff*n_fuel Eq1 = Eq(frac_goal, n_unburnt_O2/(n_out-n_H2O)) Eq2 = Eq(n_out, n_air + coeff*n_fuel) solve([Eq1, Eq2], [n_fuel, n_air])""" n_fuel = n_out*(frac_out_O2_dry*z_air_H20 - frac_out_O2_dry + z_air_O2)/(-H2O_from_fuel_n*frac_out_O2_dry + O2_burnt_n_fuel + n_delta*frac_out_O2_dry*z_air_H20 + n_delta*z_air_O2) n_air = n_out*(-H2O_from_fuel_n*frac_out_O2_dry + O2_burnt_n_fuel + n_delta*frac_out_O2_dry)/(-H2O_from_fuel_n*frac_out_O2_dry + O2_burnt_n_fuel + n_delta*(frac_out_O2_dry*z_air_H20 + z_air_O2)) # Case of two fuels known - one n out if n_out is not None and (n_fuel is None or n_air is None): """from sympy import * from sympy.abc import * frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff, n_out = symbols('frac_goal, n_air, z_O2, n_fuel, coeff, O2_coeff, n_out') solve(Eq(n_out, n_air + coeff*n_fuel), n_fuel) solve(Eq(n_out, n_air + coeff*n_fuel), n_air)""" comb_ans = combustion_products_mixture(atomss, zs_fuel, reactivities=reactivities, CASs=CASs, combustion_stoichiometries=combustion_stoichiometries) n_delta = 0.0 for k, v in comb_ans.items(): n_delta += v if n_fuel is not None: n_air = -n_delta*n_fuel + n_out elif n_air is not None: n_fuel = (-n_air + n_out)/n_delta # Compute all other properties from the air and fuel flow rate results = {'n_fuel': n_fuel, 'n_air': n_air} if n_fuel is not None and n_air is not None: ns_to_combust = [zi_air*n_air + zi_fuel*n_fuel for zi_air, zi_fuel in zip(zs_air, zs_fuel)] comb_ans = combustion_products_mixture(atomss, ns_to_combust, reactivities=reactivities, CASs=CASs, combustion_stoichiometries=combustion_stoichiometries) ns_out = combustion_products_to_list(comb_ans, CASs) # comb_fuel_only = combustion_products_mixture(atomss, [n_fuel*zi for zi in zs_fuel], reactivities=reactivities, CASs=CASs, # combustion_stoichiometries=combustion_stoichiometries) n_out = sum(ns_out) zs_out = normalize(ns_out) frac_out_O2 = zs_out[O2_index] frac_out_O2_dry = frac_out_O2/(1.0 - zs_out[H2O_index]) results['ns_out'] = ns_out results['zs_out'] = zs_out results['n_out'] = n_out results['frac_out_O2'] = frac_out_O2 results['frac_out_O2_dry'] = frac_out_O2_dry O2_in = n_air*zs_air[O2_index] + n_fuel*zs_fuel[O2_index] O2_demand = O2_in - results['ns_out'][O2_index] results['O2_excess'] = O2_in/O2_demand - 1.0 # results['O2_excess'] = n_air*z_air_O2/(comb_fuel_only['O2_required']) - 1 ratios = (None, None, None) if Vm_air is not None and Vm_fuel is not None and MW_air is not None and MW_fuel is not None: ratios = air_fuel_ratio_solver(None, Vm_air, Vm_fuel, MW_air, MW_fuel, n_air=n_air, n_fuel=n_fuel)[2:] results['mole_ratio'], results['mass_ratio'], results['volume_ratio'] = ratios return results
@mark_numba_incompatible def fuel_air_third_spec_solver(zs_air, zs_fuel, zs_third, CASs, atomss, n_third, n_fuel=None, n_air=None, n_out=None, O2_excess=None, frac_out_O2=None, frac_out_O2_dry=None, ratio=None, Vm_air=None, Vm_fuel=None, Vm_third=None, MW_air=None, MW_fuel=None, MW_third=None, ratio_basis='mass', reactivities=None, combustion_stoichiometries=None): # I believe will always require 1 intensive spec (or flow of ) # To begin - exclude n_out spec? Should be possible to solve for it/with it though. common_specs = {'zs_air': zs_air, 'CASs': CASs, 'atomss': atomss, 'n_air': n_air, 'O2_excess': O2_excess, 'frac_out_O2': frac_out_O2, 'frac_out_O2_dry': frac_out_O2_dry, 'Vm_air': Vm_air, 'MW_air': MW_air, 'ratio': ratio, 'ratio_basis': ratio_basis, 'reactivities': reactivities} O2_index = CASs.index(O2_CAS) H2O_index = CASs.index(H2O_CAS) z_air_O2 = zs_air[O2_index] z_fuel_O2 = zs_fuel[O2_index] Vm_mix, MW_mix = None, None def fix_ratios(results): if n_fuel is not None: if Vm_fuel is not None and Vm_third is not None: Vm_mix = (Vm_fuel*n_fuel + Vm_third*n_third)/(n_fuel+n_third) if MW_fuel is not None and MW_third is not None: MW_mix = (MW_fuel*n_fuel + MW_third*n_third)/(n_fuel+n_third) # Needs n_fuel and n_air to be in outer namespace ratios = (None, None, None) if Vm_air is not None and Vm_mix is not None and MW_air is not None and MW_mix is not None: ratios = air_fuel_ratio_solver(None, Vm_air, Vm_mix, MW_air, MW_mix, n_air=n_air, n_fuel=n_fuel+n_third)[2:] results['mole_ratio'], results['mass_ratio'], results['volume_ratio'] = ratios if n_fuel is not None : if Vm_fuel is not None and Vm_third is not None: Vm_mix = (Vm_fuel*n_fuel + Vm_third*n_third)/(n_fuel + n_third) if MW_fuel is not None and MW_third is not None: MW_mix = (MW_fuel*n_fuel + MW_third*n_third)/(n_fuel + n_third) # Combine the two fuels for one burn n_fuel_mix = n_fuel + n_third ns_fuel_mix = [zi*n_fuel + zj*n_third for zi, zj in zip(zs_fuel, zs_third)] zs_fuel_mix = normalize(ns_fuel_mix) mix_burn = fuel_air_spec_solver(zs_fuel=zs_fuel_mix, n_fuel=n_fuel_mix, Vm_fuel=Vm_mix, MW_fuel=MW_mix, n_out=n_out, combustion_stoichiometries=combustion_stoichiometries, **common_specs) mix_burn['n_fuel'] -= n_third return mix_burn if n_air is not None and n_fuel is None: O2_in_third = n_third*zs_third[O2_index] O2_in_orig = n_air*z_air_O2 + O2_in_third third_burn = fuel_air_spec_solver(zs_fuel=zs_third, n_fuel=n_third, Vm_fuel=Vm_third, MW_fuel=MW_third, combustion_stoichiometries=combustion_stoichiometries, **common_specs) n_air2 = third_burn['n_out'] zs_air2 = third_burn['zs_out'] O2_demand_third = O2_in_orig - third_burn['ns_out'][O2_index] extra_specs = common_specs.copy() extra_specs['n_air'] = n_air2 extra_specs['zs_air'] = zs_air2 if O2_excess is not None: """from sympy import * O2_excess, excess_sub, O2_air, O2_in_third, O2_fixed, z_fuel_O2, n_fuel, O2_coeff, O2_demand_third = symbols('O2_excess, excess_sub, O2_air, O2_in_third, O2_fixed, z_fuel_O2, n_fuel, O2_coeff, O2_demand_third') Eq1 = Eq(O2_excess, (O2_air + O2_in_third + z_fuel_O2*n_fuel)/(O2_demand_third + n_fuel*O2_coeff) - 1) Eq2 = Eq(excess_sub, (O2_fixed +z_fuel_O2*n_fuel)/(n_fuel*O2_coeff) - 1) solve([Eq1, Eq2], [excess_sub, n_fuel])""" comb_ans = combustion_products_mixture(atomss, zs_fuel, reactivities=reactivities, CASs=CASs, combustion_stoichiometries=combustion_stoichiometries) O2_burnt_n_fuel = O2_coeff = -comb_ans['O2'] + z_fuel_O2 O2_air = z_air_O2*n_air # Simpler expression for n_fuel than fake O2 excess n_fuel = (O2_air - O2_demand_third*O2_excess - O2_demand_third + O2_in_third)/(O2_coeff*O2_excess + O2_coeff - z_fuel_O2) elif ratio is not None: if ratio_basis == 'mole': n_fuel = n_air/ratio - n_third elif ratio_basis == 'mass': m_air, m_third = n_air*MW_air*1e-3, n_third*MW_third*1e-3 m_fuel = m_air/ratio - m_third n_fuel = m_fuel/(MW_fuel*1e-3) elif ratio_basis == 'volume': Q_air, Q_third = n_air*Vm_air, n_third*Vm_third Q_fuel = Q_air/ratio - Q_third n_fuel = Q_fuel/Vm_fuel fuel_burn = fuel_air_spec_solver(zs_fuel=zs_fuel, n_fuel=n_fuel, Vm_fuel=Vm_fuel, MW_fuel=MW_fuel, n_out=n_out, combustion_stoichiometries=combustion_stoichiometries, **extra_specs) O2_in_fuel = fuel_burn['n_fuel']*zs_fuel[O2_index] O2_demand_fuel = (n_air2*zs_air2[O2_index] + O2_in_fuel)/(fuel_burn['O2_excess'] + 1) # denominator and numerator are correct now, do not change vars used.. fuel_burn['O2_excess'] = (O2_in_orig + O2_in_fuel)/(O2_demand_fuel + O2_demand_third) - 1 fuel_burn['n_air'] = n_air n_fuel = fuel_burn['n_fuel'] fix_ratios(fuel_burn) # O2 excess should be wrong, same for ratios return fuel_burn if n_out is not None: # missing fuel and air, but know outlet and another spec # Burn all the third stream at O2 excess=0 # Call the solver with the first # Add the two air inlets and the combustion products third_burn = fuel_air_spec_solver(zs_fuel=zs_third, n_fuel=n_third, Vm_fuel=Vm_third, MW_fuel=MW_third, combustion_stoichiometries=combustion_stoichiometries, **common_specs) n_out_remaining = n_out - third_burn['n_out'] fuel_burn = fuel_air_spec_solver(zs_fuel=zs_fuel, n_out=n_out_remaining, Vm_fuel=Vm_fuel, MW_fuel=MW_fuel, combustion_stoichiometries=combustion_stoichiometries, **common_specs) ans = {'n_out': n_out} ans['n_air'] = n_air = third_burn['n_air'] + fuel_burn['n_air'] ans['n_fuel'] = n_fuel = fuel_burn['n_fuel'] ans['ns_out'] = [ni+nj for ni, nj in zip(third_burn['ns_out'], fuel_burn['ns_out'])] ans['zs_out'] = normalize(ans['ns_out']) ans['frac_out_O2'] = ans['ns_out'][O2_index]/n_out ans['frac_out_O2_dry'] = ans['ns_out'][O2_index]/(n_out - ans['ns_out'][H2O_index]) # O2 excess definition needs to include O2 from elsewhere! O2_in = n_air*zs_air[O2_index] + n_fuel*zs_fuel[O2_index] + n_third*zs_third[O2_index] O2_demand = O2_in - ans['ns_out'][O2_index] ans['O2_excess'] = O2_in/O2_demand - 1 fix_ratios(ans) return ans
[docs]@mark_numba_incompatible def combustion_spec_solver(zs_air, zs_fuel, zs_third, CASs, atomss, n_third, n_fuel=None, n_air=None, n_out=None, O2_excess=None, frac_out_O2=None, frac_out_O2_dry=None, ratio=None, Vm_air=None, Vm_fuel=None, Vm_third=None, MW_air=None, MW_fuel=None, MW_third=None, ratio_basis='mass', reactivities=None, combustion_stoichiometries=None): """Solves the system of equations describing a flow of air mixing with two flow of combustibles, one fixed and one potentially variable, and burning completely. All calculated variables are returned as a dictionary. The variables `Vm_air`, `Vm_fuel`, `Vm_third`, `MW_air`, `MW_fuel` and `MW_third` are only required when an air-fuel ratio is given. Howver, the ratios cannot be calculated for the other solve options without them. Parameters ---------- zs_air : list[float] Mole fractions of the air; most not contain any combustibles, [-] zs_fuel : list[float] Mole fractions of the fuel; can contain inerts and/or oxygen as well, [-] zs_third : list[float] Mole fractions of the fixed fuel flow; can contain inerts and/or oxygen as well, [-] CASs : list[str] CAS numbers of all compounds, [-] atomss : list[dict[float]] List of dictionaries of elements and their counts for all molecules in the mixtures, [-] n_third : float, optional Flow rate of third stream, (fixed) fuel flow rate, [mol/s] n_fuel : float, optional Flow rate of fuel, [mol/s] n_air : float, optional Flow rate of air, [mol/s] n_out : float, optional Flow rate of combustion products, remaining oxygen, and inerts, [mol/s] O2_excess : float, optional The excess oxygen coming out; (O2 in)/(O2 required) - 1, [-] frac_out_O2 : float, optional The mole fraction of oxygen out, [-] frac_out_O2_dry : float, optional The mole fraction of oxygen out on a dry basis, [-] ratio : float, optional Air-fuel ratio, in the specified `basis`, [-] Vm_air : float, optional Molar volume of air, [m^3/mol] Vm_fuel : float, optional Molar volume of fuel, [m^3/mol] Vm_third : float, optional Molar volume of second fuel stream, [m^3/mol] MW_air : float, optional Molecular weight of air, [g/mol] MW_fuel : float, optional Molecular weight of fuel, [g/mol] MW_third : float, optional Molecular weight of second fuel stream, [g/mol] ratio_basis : str, optional One of 'mass', 'mole', or 'volume', [-] reactivities : list[bool], optional Optional list which can be used to mark otherwise combustible compounds as incombustible and which will leave unreacted, [-] combustion_stoichiometries : list[dict[str, float]] List of return values from `combustion_stoichiometry`, can be provided if precomputed [-] Returns ------- results : dict * n_fuel : Flow rate of fuel, [mol/s] * n_air : Flow rate of air, [mol/s] * n_out : Flow rate of combustion products, remaining oxygen, and inerts, [mol/s] * O2_excess : The excess oxygen coming out; (O2 in)/(O2 required) - 1, [-] * frac_out_O2 : The mole fraction of oxygen out, [-] * frac_out_O2_dry : The mole fraction of oxygen out on a dry basis, [-] * mole_ratio : Air-fuel mole ratio, [-] * mass_ratio : Air-fuel mass ratio, [-] * volume_ratio : Air-fuel volume ratio, [-] * ns_out : Mole flow rates out, [mol/s] * zs_out : Mole fractions out, [-] Notes ----- Combustion products themselves cannot be set as unreactive. The function works so long as the flow rates, molar volumes, and molecular weights are in a consistent basis. Handling the case of the air feed containing combustibles is not implemented. Examples -------- >>> zs_air = [0.79, 0.205, 0, 0, 0, 0.0045, 0.0005] >>> zs_fuel = [0.025, 0.025, 0.85, 0.07, 0.029, 0.0005, 0.0005] >>> zs_third = [0.1, 0.005, 0.5, 0.39, 0, 0.005, 0] >>> CASs = ['7727-37-9', '7782-44-7', '74-82-8', '74-84-0', '74-98-6', '7732-18-5', '124-38-9'] >>> atomss = [{'N': 2}, {'O': 2}, {'H': 4, 'C': 1}, {'H': 6, 'C': 2}, {'H': 8, 'C': 3}, {'H': 2, 'O': 1}, {'C': 1, 'O': 2}] >>> combustion_stoichiometries = [combustion_stoichiometry(atoms) for atoms in atomss] >>> ans = combustion_spec_solver(zs_air=zs_air, zs_fuel=zs_fuel, zs_third=zs_third, CASs=CASs, atomss=atomss, n_third=1.0, n_fuel=5.0, O2_excess=0.3, Vm_air=0.02493, Vm_fuel=0.02488, Vm_third=.024, MW_air=28.79341351, MW_fuel=18.55158039, MW_third=22.0) >>> ans['n_air'] 80.6317073170732 """ TRACE_FRACTION_IN_AIR = 1e-12 if reactivities is None: reactivities = [True for i in zs_air] combustibilities = [is_combustible(CASs[i], atomss[i], reactivities[i]) for i in range(len(CASs))] zs_air_comb = [] zs_air_pure = [] n_air_comb = 0.0 n_air_pure = 0.0 air_has_combustibles = False for combustible, zi in zip(combustibilities, zs_air): if combustible and zi > TRACE_FRACTION_IN_AIR: air_has_combustibles = True zs_air_comb.append(zi) zs_air_pure.append(0.0) if n_air is not None: n_air_comb += zi*n_air else: zs_air_pure.append(zi) zs_air_comb.append(0.0) if n_air is not None: n_air_pure += zi*n_air if air_has_combustibles: try: zs_air_comb = normalize(zs_air_comb) except ZeroDivisionError: pass zs_air_pure = normalize(zs_air_pure) if not air_has_combustibles: return fuel_air_third_spec_solver(zs_air=zs_air, zs_fuel=zs_fuel, zs_third=zs_third, CASs=CASs, atomss=atomss, n_third=n_third, n_fuel=n_fuel, n_air=n_air, n_out=n_out, O2_excess=O2_excess, frac_out_O2=frac_out_O2, frac_out_O2_dry=frac_out_O2_dry, ratio=ratio, Vm_air=Vm_air, Vm_fuel=Vm_fuel, Vm_third=Vm_third, MW_air=MW_air, MW_fuel=MW_fuel, MW_third=MW_third, ratio_basis=ratio_basis, reactivities=reactivities, combustion_stoichiometries=combustion_stoichiometries) else: # Can handle air flow specs easily - burn the air, and then pass in the # burnt air composition. This includes n_air and n_out spec. # Can handle fuel and air/fuel ratio easily as is equivalent spec to air flow. # Can raise NotImplementedError("Composition of air includes combustibles")