# -*- coding: utf-8 -*-
"""Chemical Engineering Design Library (ChEDL). Utilities for process modeling.
Copyright (C) 2016, Caleb Bell <Caleb.Andrew.Bell@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 functions for lookup the following properties for a 
chemical:

* Short-term Exposure Limit (STEL)
* Time-Weighted Average Exposure Limit (TWA)  
* Celing limit for working exposure
* Whether a chemicals is absorbed thorough human skin
* Whether a chemical is a carcinogen, suspected of being a carcinogen, or has
  been identified as unlikely to be a carcinogen
  
* Flash point
* Auto ignition point
* Lower flammability limit
* Upper flammability limit

In addition, several estimation methods for chemicals without flammability
limits are provided and for calculating the flammability limits of mixtures.

This module also contains several utility functions.    

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

.. contents:: :local:

Short-term Exposure Limit
-------------------------
.. autofunction:: chemicals.safety.STEL
.. autofunction:: chemicals.safety.STEL_methods
.. autodata:: chemicals.safety.STEL_all_methods

Time-Weighted Average Exposure Limit
------------------------------------
.. autofunction:: chemicals.safety.TWA
.. autofunction:: chemicals.safety.TWA_methods
.. autodata:: chemicals.safety.TWA_all_methods

Ceiling Limit
-------------
.. autofunction:: chemicals.safety.Ceiling
.. autofunction:: chemicals.safety.Ceiling_methods
.. autodata:: chemicals.safety.Ceiling_all_methods

Skin Absorbance
---------------
.. autofunction:: chemicals.safety.Skin
.. autofunction:: chemicals.safety.Skin_methods
.. autodata:: chemicals.safety.Skin_all_methods

Carcinogenicity
---------------
.. autofunction:: chemicals.safety.Carcinogen
.. autofunction:: chemicals.safety.Carcinogen_methods
.. autodata:: chemicals.safety.Carcinogen_all_methods

Flash Point
-----------
.. autofunction:: chemicals.safety.T_flash
.. autofunction:: chemicals.safety.T_flash_methods
.. autodata:: chemicals.safety.T_flash_all_methods

Autoignition Point
------------------
.. autofunction:: chemicals.safety.T_autoignition
.. autofunction:: chemicals.safety.T_autoignition_methods
.. autodata:: chemicals.safety.T_autoignition_all_methods

Lower Flammability Limit
------------------------
.. autofunction:: chemicals.safety.LFL
.. autofunction:: chemicals.safety.LFL_methods
.. autodata:: chemicals.safety.LFL_all_methods
.. autofunction:: chemicals.safety.Suzuki_LFL
.. autofunction:: chemicals.safety.Crowl_Louvar_LFL
.. autofunction:: chemicals.safety.LFL_ISO_10156_2017

Upper Flammability Limit
------------------------
.. autofunction:: chemicals.safety.UFL
.. autofunction:: chemicals.safety.UFL_methods
.. autodata:: chemicals.safety.UFL_all_methods
.. autofunction:: chemicals.safety.Suzuki_UFL
.. autofunction:: chemicals.safety.Crowl_Louvar_UFL

Mixture Flammability Limit
--------------------------
.. autofunction:: chemicals.safety.fire_mixing

Utility Methods
---------------
.. autofunction:: chemicals.safety.ppmv_to_mgm3
.. autofunction:: chemicals.safety.mgm3_to_ppmv
.. autofunction:: chemicals.safety.NFPA_30_classification

"""

__all__ = ('ppmv_to_mgm3', 'mgm3_to_ppmv',
           'NTP_codes', 'IARC_codes', 
           'Skin_all_methods',  'Ceiling_all_methods', 'STEL_all_methods',
           'TWA_all_methods',
           'TWA_methods', 'TWA', 'STEL', 'STEL_methods', 'Ceiling', 'Ceiling_methods',
           'Skin', 'Skin_methods', 'Carcinogen_methods', 'Carcinogen_all_methods', 
           'Carcinogen', 'T_flash_all_methods', 'T_flash_methods',
           'T_flash', 'T_autoignition_methods', 'T_autoignition_all_methods',
           'T_autoignition', 'LFL_methods', 'LFL_all_methods',
           'LFL', 'UFL_methods', 'UFL_all_methods', 'UFL', 'fire_mixing', 
           'Suzuki_LFL', 'Suzuki_UFL', 
           'Crowl_Louvar_LFL', 'Crowl_Louvar_UFL', 'LFL_ISO_10156_2017',
           'NFPA_30_classification')

import os
from fluids.core import F2K
from chemicals.utils import R, none_and_length_check, normalize, PY37
from chemicals.data_reader import (register_df_source,
                                   data_source,
                                   retrieve_from_df_dict,
                                   retrieve_any_from_df_dict,
                                   list_available_methods_from_df_dict)

### Utilities 

def ppmv_to_mgm3(ppmv, MW, T=298.15, P=101325.0):
    r'''
    Converts a concentration in ppmv to units of mg/m^3. Used in
    industrial toxicology.
    
    .. math::
        \frac{mg}{m^3} = \frac{ppmv\cdot P}{RT}\cdot \frac{MW}{1000}
    
    Parameters
    ----------
    ppmv : float
        Concentration of a component in a gas mixure [parts per million,
        volumetric]
    MW : float
        Molecular weight of the trace gas [g/mol]
    T : float, optional
        Temperature of the gas at which the ppmv is reported, [K]
    P : float, optional
        Pressure of the gas at which the ppmv is reported, [Pa]
    
    Returns
    -------
    mgm3 : float
        Concentration of a substance in an ideal gas mixture [mg/m^3]
    
    Notes
    -----
    The term P/(RT)/1000 converts to 0.040874 at STP. Its inverse is reported
    as 24.45 in [1]_.
    
    Examples
    --------
    >>> ppmv_to_mgm3(1.0, 40.0)
    1.6349617809430446
    
    References
    ----------
    .. [1] ACGIH. Industrial Ventilation: A Manual of Recommended Practice,
       23rd Edition. American Conference of Governmental and Industrial
       Hygenists, 2004.
    '''
    parts = ppmv*1E-6
    n = parts*P/(R*T)
    mgm3 = MW*n*1000  # mol toxin /m^3 * g/mol toxis * 1000 mg/g
    return mgm3

def mgm3_to_ppmv(mgm3, MW, T=298.15, P=101325.):
    r'''
    Converts a concentration in  mg/m^3 to units of ppmv. Used in
    industrial toxicology.
    
    .. math::
        ppmv = \frac{1000RT}{MW\cdot P} \cdot \frac{mg}{m^3}
    
    Parameters
    ----------
    mgm3 : float
        Concentration of a substance in an ideal gas mixture [mg/m^3]
    MW : float
        Molecular weight of the trace gas [g/mol]
    T : float, optional
        Temperature of the gas at which the ppmv is reported, [K]
    P : float, optional
        Pressure of the gas at which the ppmv is reported, [Pa]
    
    Returns
    -------
    ppmv : float
        Concentration of a component in a gas mixure [parts per million,
        volumetric]
    
    Notes
    -----
    The term P/(RT)/1000 converts to 0.040874 at STP. Its inverse is reported
    as 24.45 in [1]_.
    
    Examples
    --------
    >>> mgm3_to_ppmv(1.635, 40.0)
    1.0000233761164334
    
    References
    ----------
    .. [1] ACGIH. Industrial Ventilation: A Manual of Recommended Practice,
       23rd Edition. American Conference of Governmental and Industrial
       Hygenists, 2004.
    '''
    n = mgm3/MW/1000.
    parts = n*R*T/P
    ppm = parts/1E-6
    return ppm

### Data


NTP_codes = {1: 'Known', 2: 'Reasonably Anticipated'}
IARC_codes = {1: 'Carcinogenic to humans (1)',
              11: 'Probably carcinogenic to humans (2A)',  # 2A
              12: 'Possibly carcinogenic to humans (2B)',  # 2B
              3: 'Not classifiable as to its carcinogenicity to humans (3)',
              4: 'Probably not carcinogenic to humans (4)'}

folder = os.path.join(os.path.dirname(__file__), 'Safety')
register_df_source(folder, 'NFPA 497 2008.tsv')
register_df_source(folder, 'IS IEC 60079-20-1 2010.tsv')
register_df_source(folder, 'DIPPR T_flash Serat.csv')
register_df_source(folder, 'National Toxicology Program Carcinogens.tsv')
register_df_source(folder, 'IARC Carcinogen Database.tsv')
_safety_data_loaded = False


IEC = 'IEC 60079-20-1 (2010)'
NFPA = 'NFPA 497 (2008)'
SERAT = 'Serat DIPPR (2017)'

SUZUKI = 'Suzuki (1994)'
CROWLLOUVAR = 'Crowl and Louvar (2001)'

def _load_safety_data():
    global Ontario_exposure_limits_dict, NFPA_2008_data, IEC_2010_data
    global DIPPR_SERAT_data, NTP_data, IARC_data, Tflash_sources
    global Tautoignition_sources, LFL_sources, UFL_sources, _safety_data_loaded
    import json
    from io import open
    file = os.path.join(folder, 'Ontario Exposure Limits.json')
    with open(file, 'r') as stream: 
        Ontario_exposure_limits_dict = json.load(stream)
    NFPA_2008_data = data_source('NFPA 497 2008.tsv')
    IEC_2010_data = data_source('IS IEC 60079-20-1 2010.tsv')
    DIPPR_SERAT_data = data_source('DIPPR T_flash Serat.csv')
    NTP_data = data_source('National Toxicology Program Carcinogens.tsv')
    IARC_data = data_source('IARC Carcinogen Database.tsv')
    Tflash_sources = {IEC: IEC_2010_data,
                      NFPA: NFPA_2008_data,
                      SERAT: DIPPR_SERAT_data}
    Tautoignition_sources = {IEC: IEC_2010_data,
                             NFPA: NFPA_2008_data}
    LFL_sources = Tautoignition_sources.copy()
    UFL_sources = Tautoignition_sources.copy()
    _safety_data_loaded = True

if PY37:
    def __getattr__(name):
        if name in ('Ontario_exposure_limits_dict', 'NFPA_2008_data', 'IEC_2010_data',
                    'DIPPR_SERAT_data'):
            _load_safety_data()
            return globals()[name]
        raise AttributeError("module %s has no attribute %s" %(__name__, name))
else: # pragma: no cover
    _load_safety_data()

# # Used to read Ontario Expore Limits data from original file (DO NOT DELETE!)
# Ontario_exposure_limits_dict = {}
# def str_to_ppm_mgm3(line, MW):  # pragma: no cover
#     if not line:
#         return None, None
#     if 'ppm' in line:
#         _ppm = float(line.split('ppm')[0])
#         try:
#             _mgm3 = ppmv_to_mgm3(_ppm, MW)
#         except:
#             _mgm3 = None
#     elif 'mg/m3' in line:
#         _mgm3 = float(line.split('mg/m3')[0])
#         try:
#             _ppm = mgm3_to_ppmv(_mgm3, MW)
#         except:
#             _ppm = None
#     if not _ppm and not _mgm3:
#         raise Exception('failure in function')
#     return (_ppm, _mgm3)

# with open(os.path.join(folder, 'Ontario Exposure Limits.tsv'), encoding='utf-8') as f:
#     '''Read in a dict of TWAs, STELs, and Ceiling Limits. The data source
#     is the Ontario Labor Website. They have obtained their data in part from
#     their own reviews, and also from ACGIH.
#     Warning: The lowest value is taken, when multiple units or different forms
#              of a compound are listed.
#     Note that each province has a different set of values, but these serve
#     as general values.
#     '''
#     next(f)
#     for line in f:
#         values = to_num(line.strip('\n').split('\t'))
        
#         if values[0]:
#             if type(values[6]) == str:
#                 MWs = [float(i) if i != '' else None for i in values[6].split(';')]
#             elif values[6] is None:
#                 MWs = [None]
#             elif type(values[6]) == float:
#                 MWs = [values[6]]
#             else:
#                 MWs = [None]
                
#             for i, CASRN in enumerate(values[0].split(';')):
#                 try:
#                     MWi = MWs[i]
#                 except IndexError:
#                     MWi = None


#                 _ppm_TWA, _mgm3_TWA = str_to_ppm_mgm3(values[2], MWi)
#                 _ppm_STEL, _mgm3_STEL = str_to_ppm_mgm3(values[3], MWi)
#                 _ppm_C, _mgm3_C = str_to_ppm_mgm3(values[4], MWi)
                                    
#                 if values[5] == 'Skin':
#                     _skin = True
#                 else:
#                     _skin = False
                    
                    
#                 Ontario_exposure_limits_dict[CASRN] = {"Name": values[1],  "TWA (ppm)": _ppm_TWA,
#                 "TWA (mg/m^3)": _mgm3_TWA, "STEL (ppm)": _ppm_STEL,
#                 "STEL (mg/m^3)": _mgm3_STEL, "Ceiling (ppm)": _ppm_C,
#                 "Ceiling (mg/m^3)": _mgm3_C, "Skin":_skin, "MW": MWi} 


#TODO: Add CRC exposure limits. Note that functions should be used.
#_CRCExposureLimits = {}
#with open(os.path.join(folder,'CRC Exposure Limits.csv')) as f:
#    '''Read in a dict of TWAs and STELs. The data source
#    is the CRC Handbook.. They have obtained their data from
#    NIOSH, and OSHA Chemical Information Manual, and ACGIH.
#    '''
#    f.next()
#    for line in f:
#        values = to_num(line.strip('\n').split('\t'))
#        _ppm_TWA, _mgm3_TWA = str_to_ppm_mgm3(values[2], CASRN.strip())
#        _ppm_STEL, _mgm3_STEL = str_to_ppm_mgm3(values[3], CASRN.strip())
#        _CRCExposureLimits[CASRN] = {"Name": values[1],  "TWA (ppm)": _ppm_TWA,
#        "TWA (mg/m^3)": _mgm3_TWA, "STEL (ppm)": _ppm_STEL,
#        "STEL (mg/m^3)": _mgm3_STEL}
#del _ppm_TWA, _mgm3_TWA, _ppm_STEL, _mgm3_STEL, _ppm_C, _mgm3_C, status
#print Ontario_exposure_limits_dict['109-73-9']
##{'STEL (ppm)': None, 'Name': 'n-Butylamine [109-73-9]', 'Ceiling (mg/m^3)': 14.956408997955013, 'Ceiling (ppm)': 5.0, 'TWA (mg/m^3)': None, 'STEL (mg/m^3)': None, 'TWA (ppm)': None}
#print Ontario_exposure_limits_dict['34590-94-8']
#{'STEL (ppm)': 150.0, 'Name': '(2-Methoxymethylethoxy) propanol (DPGME) [34590-94-8]', 'Ceiling (mg/m^3)': None, 'Ceiling (ppm)': None, 'TWA (mg/m^3)': None, 'STEL (mg/m^3)': None, 'TWA (ppm)': 100.0}


### OSHA exposure limit functions

ONTARIO = 'Ontario Limits'
TWA_all_methods = (ONTARIO,)
'''Tuple of method name keys. See the :obj:`TWA` for the actual references'''

STEL_all_methods = (ONTARIO,)
'''Tuple of method name keys. See the :obj:`STEL` for the actual references'''

Ceiling_all_methods = (ONTARIO,)
'''Tuple of method name keys. See the :obj:`Ceiling` for the actual references'''

Skin_all_methods = (ONTARIO,)
'''Tuple of method name keys. See the :obj:`Skin` for the actual references'''

def TWA_methods(CASRN):
    """Return all methods available to obtain the Time-Weighted Average exposure
    limits (TWA) for the desired chemical.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain TWA with the given inputs.

    See Also
    --------
    TWA
    
    Examples
    --------
    >>> TWA_methods('71-43-2')
    ['Ontario Limits']
    """
    if not _safety_data_loaded: _load_safety_data()
    if CASRN in Ontario_exposure_limits_dict:
        data = Ontario_exposure_limits_dict[CASRN]
        if (data["TWA (ppm)"] or data["TWA (mg/m^3)"]): return [ONTARIO]
    return []
    
def TWA(CASRN, method=None):
    """Return the Time-Weighted Average exposure
    limits (TWA) for the desired chemical if it is available.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]
    method : str
        Name of method to use, [-]

    Returns
    -------
    TWA : float
        Time-Weighted Average exposure, [ppm or mg/m^3]
    units : str
        One of ppm or mg/m^3, [-]
    
    Notes
    -----
    The ppm value is preferentially returned if both are available. While they
    can be converted in specific cases, it is better to work with the specified
    units of the original source.
    
    Examples
    --------
    >>> TWA('98-00-0')
    (10.0, 'ppm')
    >>> TWA('1303-00-0')
    (5.0742430905659505e-05, 'ppm')
    """
    if not _safety_data_loaded: _load_safety_data()
    if not method or method == ONTARIO:
        if CASRN in Ontario_exposure_limits_dict:
            data = Ontario_exposure_limits_dict[CASRN]
            value = data["TWA (ppm)"]
            if value: return value, 'ppm'
            value = data["TWA (mg/m^3)"]
            if value: return value, 'mg/m^3'
    else:
        raise ValueError('Invalid method: %s, allowed methods are %s' %(
                         method, TWA_all_methods))

def STEL_methods(CASRN):
    """Return all methods available to obtain STEL for the desired chemical.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain STEL with the given inputs.

    See Also
    --------
    STEL
    """
    if not _safety_data_loaded: _load_safety_data()
    if CASRN in Ontario_exposure_limits_dict:
        data = Ontario_exposure_limits_dict[CASRN]
        if (data["STEL (ppm)"] or data["STEL (mg/m^3)"]): return [ONTARIO]
    return []

def STEL(CASRN, method=None):
    """This function handles the retrieval of Short-term Exposure Limit (STEL)
    on worker exposure to dangerous chemicals.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]
    method : str
        Name of method to use, [-]

    Returns
    -------
    STEL : float
        Short-term Exposure Limit, [ppm or mg/m^3]
    units : str
        One of ppm or mg/m^3, [-]
    
    Notes
    -----
    The ppm value is preferentially returned if both are available. While they
    can be converted in specific cases, it is better to work with the specified
    units of the original source.

    Examples
    --------
    >>> STEL('67-64-1')
    (750.0, 'ppm')
    >>> STEL('7664-38-2')
    (0.7489774978301237, 'ppm')
    >>> STEL('55720-99-5')
    (2.0, 'mg/m^3')
    """
    if not _safety_data_loaded: _load_safety_data()
    if not method or method == ONTARIO:
        if CASRN in Ontario_exposure_limits_dict:
            data = Ontario_exposure_limits_dict[CASRN]
            value = data["STEL (ppm)"]
            if value: return value, 'ppm'
            value = data["STEL (mg/m^3)"]
            if value: return value, 'mg/m^3'
    else:
        raise ValueError('Invalid method: %s, allowed methods are %s' %(
                         method, TWA_all_methods))

def Ceiling_methods(CASRN):
    """Return all methods available to obtain Ceiling limits for the desired
    chemical.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain Ceiling limits with the given inputs.

    See Also
    --------
    Ceiling
    """
    if not _safety_data_loaded: _load_safety_data()
    if CASRN in Ontario_exposure_limits_dict:
        data = Ontario_exposure_limits_dict[CASRN]
        if (data["Ceiling (ppm)"] or data["Ceiling (mg/m^3)"]): return [ONTARIO]
    return []

def Ceiling(CASRN, method=None):
    """This function handles the retrieval of Ceiling limits on worker exposure
    to dangerous chemicals.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]
    method : str
        Name of method to use, [-]

    Returns
    -------
    Ceiling : float
        Ceiling Limit, [ppm or mg/m^3]
    units : str
        One of ppm or mg/m^3, [-]

    Examples
    --------
    >>> Ceiling('75-07-0')
    (25.0, 'ppm')
    >>> Ceiling('1395-21-7')
    (6e-05, 'mg/m^3')
    """
    if not _safety_data_loaded: _load_safety_data()
    if not method or method == ONTARIO:
        if CASRN in Ontario_exposure_limits_dict:
            data = Ontario_exposure_limits_dict[CASRN]
            value = data["Ceiling (ppm)"]
            if value: return value, 'ppm'
            value = data["Ceiling (mg/m^3)"]
            if value: return value, 'mg/m^3'
    else:
        raise ValueError('Invalid method: %s, allowed methods are %s' %(
                         method, list(Ontario_exposure_limits_dict)))

def Skin_methods(CASRN):
    """Return all methods available to obtain whether or not a chemical can be
    absorbed through the skin.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain whether or not a chemical can
        be absorbed through the skin.

    See Also
    --------
    Skin
    """
    return [ONTARIO] if CASRN in Ontario_exposure_limits_dict else []

def Skin(CASRN, method=None):
    """This function handles the retrieval of whether or not a chemical can be
    absorbed through the skin, relevant to chemical safety calculations.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]
    method : str
        Name of method to use, [-]

    Returns
    -------
    skin : bool
        Whether or not the substance is absorbed through human skin, [-]

    Examples
    --------
    >>> Skin('108-94-1')
    True
    >>> Skin('1395-21-7')
    False
    """
    if not _safety_data_loaded: _load_safety_data()
    if not method or method == ONTARIO:
        if CASRN in Ontario_exposure_limits_dict:
            return Ontario_exposure_limits_dict[CASRN]["Skin"]
    else:
        raise ValueError('Invalid method: %s, allowed methods are %s' %(
                         method, TWA_all_methods))

### Carcinogen functions

IARC = 'International Agency for Research on Cancer'
NTP = 'National Toxicology Program 13th Report on Carcinogens'
UNLISTED = 'Unlisted'
COMBINED = 'Combined'

Carcinogen_all_methods = (IARC, NTP)
'''Tuple of method name keys. See the :obj:`Carcinogen` for the actual references'''

def Carcinogen_methods(CASRN):
    """Return all methods available to obtain Carcinogen listings for the
    desired chemical.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain Carcinogen listings with the given inputs.

    See Also
    --------
    Carcinogen
    """
    return list(Carcinogen_all_methods)

def Carcinogen(CASRN, method=None):
    r'''Looks up if a chemical is listed as a carcinogen or not according to
    either a specifc method or with all methods.
    Returns either the status as a string for a specified method, or the
    status of the chemical in all available data sources, in the format
    {source: status}.
    
    Parameters
    ----------
    CASRN : str
        CASRN [-]
        
    Returns
    -------
    status : str or dict
        Carcinogen status information [-].
        
    Other Parameters
    ----------------
    method : string, optional
        A string for the method name to use, as defined in the variable,
        `Carcinogen_all_methods`.
        
    Notes
    -----
    Supported methods are:
        * **IARC**: International Agency for Research on Cancer, [1]_. As
          extracted with a last update of  February 22, 2016. Has listing
          information of 863 chemicals with CAS numbers. Chemicals without
          CAS numbers not included here. If two listings for the same CAS
          were available, the harshest rating was used. If two
          listings were available published at different times, the latest
          value was used. All else equal, the most pessimistic value was used.
        * **NTP**: National Toxicology Program, [2]_. Has data on 228
          chemicals.
    
    Examples
    --------
    >>> Carcinogen('61-82-5')
    {'International Agency for Research on Cancer': 'Not classifiable as to its carcinogenicity to humans (3)', 'National Toxicology Program 13th Report on Carcinogens': 'Reasonably Anticipated'}
    
    References
    ----------
    .. [1] International Agency for Research on Cancer. Agents Classified by
       the IARC Monographs, Volumes 1-115. Lyon, France: IARC; 2020 Available
       from: http://monographs.iarc.fr/ENG/Classification/
    .. [2] NTP (National Toxicology Program). 2016. Report on Carcinogens,
       Fourteenth Edition. Research Triangle Park, NC: U.S. Department of
       Health and Human Services, Public Health Service.
       https://ntp.niehs.nih.gov/whatwestudy/assessments/cancer/roc/index.html
    '''
    if not _safety_data_loaded: _load_safety_data()
    if not method:
        return {
            IARC: IARC_codes[IARC_data.at[CASRN, 'group']] if CASRN in IARC_data.index else UNLISTED,
            NTP: NTP_codes[NTP_data.at[CASRN, 'Listing']] if CASRN in NTP_data.index else UNLISTED
        }
    if method == IARC:
        if CASRN in IARC_data.index:
            return IARC_codes[IARC_data.at[CASRN, 'group']]
    elif method == NTP:
        if CASRN in NTP_data.index:
            return NTP_codes[NTP_data.at[CASRN, 'Listing']]
    else:
        raise ValueError('Invalid method: %s, allowed methods are %s' %(
                       method, Carcinogen_all_methods))
    return UNLISTED


### Fire-related functions


T_flash_all_methods = (IEC, NFPA, SERAT)
'''Tuple of method name keys. See the :obj:`T_flash` for the actual references'''

def T_flash_methods(CASRN):
    """Return all methods available to obtain T_flash for the desired chemical.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain T_flash with the given inputs.

    See Also
    --------
    T_flash
    """
    if not _safety_data_loaded: _load_safety_data()
    return list_available_methods_from_df_dict(Tflash_sources, CASRN, 'T_flash')

def T_flash(CASRN, method=None):
    r'''
    This function handles the retrieval or calculation of a chemical's
    flash point. Lookup is based on CASRNs. No predictive methods are currently
    implemented. Will automatically select a data source to use if no method
    is provided; returns None if the data is not available.
    
    Examples
    --------
    >>> T_flash(CASRN='64-17-5')
    285.15
    
    Parameters
    ----------
    CASRN : str
        CASRN [-]
    
    Returns
    -------
    T_flash : float
        Flash point of the chemical, [K]
    
    Other Parameters
    ----------------
    method : string, optional
        A string for the method name to use, as defined in the variable,
        `T_flash_all_methods`,
    
    Notes
    -----
    Preferred source is 'IEC 60079-20-1 (2010)' [1]_, with the secondary source
    'NFPA 497 (2008)' [2]_ having very similar data. A third source 
    'Serat DIPPR (2017)' [3]_ provides third hand experimental but evaluated 
    data from the DIPPR database, version unspecified, for 870 compounds.
    
    The predicted values from the DIPPR databank are also available in the
    supporting material in [3]_, but are not included.
    
    See Also
    --------
    T_flash_methods
    
    References
    ----------
    .. [1] IEC. "IEC 60079-20-1:2010 Explosive atmospheres - Part 20-1:
       Material characteristics for gas and vapour classification - Test
       methods and data." https://webstore.iec.ch/publication/635. See also
       https://law.resource.org/pub/in/bis/S05/is.iec.60079.20.1.2010.pdf
    .. [2] National Fire Protection Association. NFPA 497: Recommended
       Practice for the Classification of Flammable Liquids, Gases, or Vapors
       and of Hazardous. NFPA, 2008.
    .. [3] Serat, Fatima Zohra, Ali Mustapha Benkouider, Ahmed Yahiaoui, and 
       Farid Bagui. "Nonlinear Group Contribution Model for the Prediction of 
       Flash Points Using Normal Boiling Points." Fluid Phase Equilibria 449 
       (October 15, 2017): 52-59. doi:10.1016/j.fluid.2017.06.008.
    
    '''
    if not _safety_data_loaded: _load_safety_data()
    if method:
        return retrieve_from_df_dict(Tflash_sources, CASRN, 'T_flash', method)
    else:
        return retrieve_any_from_df_dict(Tflash_sources, CASRN, 'T_flash')


T_autoignition_all_methods = (IEC, NFPA)
'''Tuple of method name keys. See the :obj:`T_autoignition` for the actual references'''

def T_autoignition_methods(CASRN):
    """Return all methods available to obtain T_autoignition for the desired
    chemical.

    Parameters
    ----------
    CASRN : str
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain T_autoignition with the given inputs.

    See Also
    --------
    T_autoignition
    """
    if not _safety_data_loaded: _load_safety_data()
    return list_available_methods_from_df_dict(Tautoignition_sources, CASRN, 'T_autoignition')

def T_autoignition(CASRN, method=None):
    r'''
    This function handles the retrieval or calculation of a chemical's
    autoifnition temperature. Lookup is based on CASRNs. No predictive methods
    are currently implemented. Will automatically select a data source to use
    if no Method is provided; returns None if the data is not available.
    
    Parameters
    ----------
    CASRN : str
        CASRN [-]
    
    Returns
    -------
    Tautoignition : float
        Autoignition point of the chemical, [K].
    
    Other Parameters
    ----------------
    method : string, optional
        A string for the method name to use, as defined in the variable,
        `T_autoignition_all_methods`.
    
    Examples
    --------
    >>> T_autoignition(CASRN='71-43-2')
    771.15
    
    Notes
    -----
    Preferred source is 'IEC 60079-20-1 (2010)' [1]_, with the secondary source
    'NFPA 497 (2008)' [2]_ having very similar data.
    
    See Also
    --------
    T_autoignition_methods
    
    References
    ----------
    .. [1] IEC. “IEC 60079-20-1:2010 Explosive atmospheres - Part 20-1:
       Material characteristics for gas and vapour classification - Test
       methods and data.” https://webstore.iec.ch/publication/635. See also
       https://law.resource.org/pub/in/bis/S05/is.iec.60079.20.1.2010.pdf
    .. [2] National Fire Protection Association. NFPA 497: Recommended
       Practice for the Classification of Flammable Liquids, Gases, or Vapors
       and of Hazardous. NFPA, 2008.
    '''
    if not _safety_data_loaded: _load_safety_data()
    if method:
        return retrieve_from_df_dict(Tautoignition_sources, CASRN, 'T_autoignition', method)
    else:
        return retrieve_any_from_df_dict(Tautoignition_sources, CASRN, 'T_autoignition')



LFL_all_methods = (IEC, NFPA, SUZUKI, CROWLLOUVAR)
'''Tuple of method name keys. See the :obj:`LFL` for the actual references'''

def LFL_methods(Hc=None, atoms=None, CASRN=''):
    """Return all methods available to obtain LFL for the desired chemical.

    Parameters
    ----------
    Hc : float, optional
        Heat of combustion of gas [J/mol].
    atoms : dict, optional
        Dictionary of atoms and atom counts.
    CASRN : str, optional
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain LFL with the given inputs.

    See Also
    --------
    LFL
    
    Examples
    --------
    Methane
    
    >>> LFL_methods(Hc=-890590.0, atoms={'C': 1, 'H': 4}, CASRN='74-82-8')
    ['IEC 60079-20-1 (2010)', 'NFPA 497 (2008)', 'Suzuki (1994)', 'Crowl and Louvar (2001)']
    """
    if not _safety_data_loaded: _load_safety_data()
    methods = list_available_methods_from_df_dict(LFL_sources, CASRN, 'LFL')
    if Hc is not None:
        methods.append(SUZUKI)
    if atoms is not None:
        methods.append(CROWLLOUVAR)
    return methods

def LFL(Hc=None, atoms=None, CASRN='', method=None):
    r'''This function handles the retrieval or calculation of a chemical's
    Lower Flammability Limit. 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.
    
    Parameters
    ----------
    Hc : float, optional
        Heat of combustion of gas [J/mol].
    atoms : dict, optional
        Dictionary of atoms and atom counts.
    CASRN : str, optional
        CASRN, [-]
    
    Returns
    -------
    LFL : float
        Lower flammability limit of the gas in an atmosphere at STP, [mole fraction].
    
    Other Parameters
    ----------------
    method : string, optional
        A string for the method name to use, as defined in the variable,
        `LFL_all_methods`.
    
    Examples
    --------
    >>> LFL(CASRN='71-43-2')
    0.012
    >>> LFL(Hc=-890590.0, atoms={'C': 1, 'H': 4}, CASRN='74-82-8')
    0.044000000000000004
    
    Notes
    -----
    Preferred source is 'IEC 60079-20-1 (2010)' [1]_, with the secondary source
    'NFPA 497 (2008)' [2]_ having very similar data. If the heat of combustion
    is provided, the estimation method :obj:`Suzuki_LFL` can be used. If the atoms
    of the molecule are available, the method :obj:`Crowl_Louvar_LFL` can be used.
    
    References
    ----------
    .. [1] IEC. “IEC 60079-20-1:2010 Explosive atmospheres - Part 20-1:
       Material characteristics for gas and vapour classification - Test
       methods and data.” https://webstore.iec.ch/publication/635. See also
       https://law.resource.org/pub/in/bis/S05/is.iec.60079.20.1.2010.pdf
    .. [2] National Fire Protection Association. NFPA 497: Recommended
       Practice for the Classification of Flammable Liquids, Gases, or Vapors
       and of Hazardous. NFPA, 2008.
    
    '''
    if not _safety_data_loaded: _load_safety_data()
    if not method:
        LFL = retrieve_any_from_df_dict(LFL_sources, CASRN, 'LFL') 
        if not LFL:
            if Hc: LFL = Suzuki_LFL(Hc)
            elif atoms: LFL = Crowl_Louvar_LFL(atoms)
        return LFL
    elif method == SUZUKI:
        return Suzuki_LFL(Hc)
    elif method == CROWLLOUVAR:
        return Crowl_Louvar_LFL(atoms)
    else:
        return retrieve_from_df_dict(LFL_sources, CASRN, 'LFL', method) 
    
UFL_all_methods = (IEC, NFPA, SUZUKI, CROWLLOUVAR)
'''Tuple of method name keys. See the :obj:`UFL` for the actual references'''

def UFL_methods(Hc=None, atoms=None, CASRN=''):
    """Return all methods available to obtain UFL for the desired chemical.

    Parameters
    ----------
    Hc : float, optional
        Heat of combustion of gas [J/mol].
    atoms : dict, optional
        Dictionary of atoms and atom counts.
    CASRN : str, optional
        CASRN, [-]

    Returns
    -------
    methods : list[str]
        Methods which can be used to obtain UFL with the given inputs.

    See Also
    --------
    UFL
    
    Examples
    --------
    Methane
    
    >>> UFL_methods(Hc=-890590.0, atoms={'C': 1, 'H': 4}, CASRN='74-82-8')
    ['IEC 60079-20-1 (2010)', 'NFPA 497 (2008)', 'Suzuki (1994)', 'Crowl and Louvar (2001)']
    """
    if not _safety_data_loaded: _load_safety_data()
    methods = list_available_methods_from_df_dict(UFL_sources, CASRN, 'UFL')
    if Hc is not None:
        methods.append(SUZUKI)
    if atoms is not None:
        methods.append(CROWLLOUVAR)
    return methods

def UFL(Hc=None, atoms=None, CASRN='', method=None):
    r'''This function handles the retrieval or calculation of a chemical's
    Upper Flammability Limit. Lookup is based on CASRNs. Two predictive methods
    are currently implemented. Will automatically select a data source to use
    if no Method is provided; returns None if the data is not available.
    
    Examples
    --------
    >>> UFL(CASRN='71-43-2')
    0.086
    
    Methane
    
    >>> UFL(Hc=-890590.0, atoms={'C': 1, 'H': 4}, CASRN='74-82-8')
    0.17
    
    Parameters
    ----------
    Hc : float, optional
        Heat of combustion of gas [J/mol]
    atoms : dict, optional
        Dictionary of atoms and atom counts
    CASRN : str, optional
        CASRN [-]
    
    Returns
    -------
    UFL : float
        Upper flammability limit of the gas in an atmosphere at STP, [mole fraction]
    
    Other Parameters
    ----------------
    method : string, optional
        A string for the method name to use, as defined in the variable,
        `UFL_all_methods`.
    
    Notes
    -----
    Preferred source is 'IEC 60079-20-1 (2010)' [1]_, with the secondary source
    'NFPA 497 (2008)' [2]_ having very similar data. If the heat of combustion
    is provided, the estimation method :obj:`Suzuki_UFL` can be used. If the atoms
    of the molecule are available, the method :obj:`Crowl_Louvar_UFL` can be used.
    
    References
    ----------
    .. [1] IEC. “IEC 60079-20-1:2010 Explosive atmospheres - Part 20-1:
       Material characteristics for gas and vapour classification - Test
       methods and data.” https://webstore.iec.ch/publication/635. See also
       https://law.resource.org/pub/in/bis/S05/is.iec.60079.20.1.2010.pdf
    .. [2] National Fire Protection Association. NFPA 497: Recommended
       Practice for the Classification of Flammable Liquids, Gases, or Vapors
       and of Hazardous. NFPA, 2008.
    
    '''
    if not _safety_data_loaded: _load_safety_data()
    if not method: 
        UFL = retrieve_any_from_df_dict(UFL_sources, CASRN, 'UFL') 
        if not UFL:
            if Hc is not None: UFL = Suzuki_UFL(Hc)
            elif atoms is not None: UFL = Crowl_Louvar_UFL(atoms)
        return UFL
    elif method == SUZUKI:
        return Suzuki_UFL(Hc)
    elif method == CROWLLOUVAR:
        return Crowl_Louvar_UFL(atoms)
    else:
        return retrieve_from_df_dict(UFL_sources, CASRN, 'UFL', method) 

ISO_10156_2017_Kks = {
    '7782-44-7': 1.0, # O2,
    '7727-37-9': 1.0, # N2
    '124-38-9': 1.5, # CO2
    '7440-59-7': 0.9, # He
    '7440-37-1': 0.55, # Ar
    '7440-01-9': 0.7, # Ne
    '7439-90-9': 0.5, # Kr
    '7440-63-3': 0.5, # Xe
    '7446-09-5': 1.5, # SO2
    '2551-62-4': 4.0, # SF6
    '75-73-0': 2.0, # CF4
    '76-19-7': 1.5, # C3F8
    '354-33-6': 3.5, # C2HF5
}

def LFL_ISO_10156_2017(zs, LFLs, CASs):
    r'''Calculate the lower flammability limit of a mixture of combustible gases
    and inert gases according to ISO 10156 (2017) [1]_. 
    
    .. math::
        \text{LFL} = \frac{1}{\sum_{i=1}^{n_{combustible}}\frac{A_i}{\text{LFL}_i'}}
    
    .. math::
        \text{LFL}_i' = \frac{1 - \text{LFL}_m' - (1 - K)
        \frac{\sum_j^{n_{inert}} B_j}{\sum_j^{n_{combustible}} A_j} \text{LFL}_m'
        }
        {100 - \text{LFL}_m'}\text{LFL}_i
        
    .. math::
        K = \sum_i^{n_{inert}} z_i K_k
        
    The `B` sum is the total mole fraction of all inert gas compounds;
    and the `A` sum is the total mole fraction of all combustible compounds.
    :math:`K_k` are the looked up inert gas coefficients.
    :math:`\text{LFL}_m'` is calculated as the Le Chatelier's lower
    flammability limit if there were no inert gases in the mixture.
    
    Parameters
    ----------
    zs : list[float]
        Mole fractions of all components in a gas including inerts, [-]
    LFLs : list[float]
        Lower or upper flammability limits for each flammable component in a
        gas, [-]
    CASs : list[str]
        CAS numbers of each compound; required to look up inert gas factors, [-]
    
    Returns
    -------
    LFL : float
        Lower or flammability limit of a gas mixture, [-]
    
    Notes
    -----
    Inert gas parameters are available for O2, N2, CO2, He, Ar, Ne, Kr, Xe,
    SO2, SF6, CF4, C3F8, and C2HF5.
    
    Examples
    --------
    All the sample problems from [1]_ have been implemented as tests.
    
    >>> zs = [.15, .15, .3, .35+.05*.79, .05*.21]
    >>> LFLs = [.04, .044, None, None, None]
    >>> CASs = ['1333-74-0', '74-82-8', '124-38-9', '7727-37-9', '7782-44-7']
    >>> LFL_ISO_10156_2017(zs, LFLs, CASs)
    0.14273722742907632

    References
    ----------
    .. [1] Standardization, International Organization for. ISO 10156: 2017 :
       Gas Cylinders - Gases and Gas Mixtures - Determination of Fire Potential
       and Oxidizing Ability for the Selection of Cylinder Valve Outlets, 2017.
    '''
    N = len(zs)
    has_inerts = False
    for CAS in CASs:
        if CAS in ISO_10156_2017_Kks:
            has_inerts = True
            break
    
    if has_inerts:
        combustible_idxs = []
        combustible_zs = []
        for i, CAS in enumerate(CASs):
            # Combustible
            if CAS not in ISO_10156_2017_Kks:
                combustible_idxs.append(i)
                combustible_zs.append(zs[i])

        combustible_zs_norm = normalize(combustible_zs)
        Lm_prime = 0.0
        for idx, zi in zip(combustible_idxs, combustible_zs_norm):
            Lm_prime += zi/LFLs[idx]
        Lm_prime = 1.0/Lm_prime
        
        combustible_frac = sum(combustible_zs)
        inert_frac = 1.0 - combustible_frac
        K = 0.0
        for i, CAS in enumerate(CASs):
            if CAS in ISO_10156_2017_Kks:
                K += ISO_10156_2017_Kks[CAS]*zs[i]
        K /= inert_frac
        
        factor = ((1.0 - Lm_prime - (1.0 - K)*inert_frac/combustible_frac*Lm_prime)
                  /(1.0 - Lm_prime))
        Lm = 0.0
        for idx, zi in zip(combustible_idxs, combustible_zs):
            Lm += zi/(factor*LFLs[idx])
        Lm = 1.0/Lm
        return Lm

    tot = 0.0
    for i in range(len(zs)):
        tot += zs[i]/LFLs[i]
    return 1.0/tot

def fire_mixing(ys, FLs):
    '''Le Chatelier's mixing rule for lower and upper flammability limits of
    mixtures of gases.
    
    Parameters
    ----------
    ys : list[float]
        Normalized mole fractions of all flammable components in a gas, [-]
    FLs : list[float]
        Lower or upper flammability limits for each flammable component in a
        gas, [-]
    
    Returns
    -------
    FL : float
        Lower or upper flammability limit of a gas, [-]
    
    Notes
    -----
    This equation has a higher accuracy for lower flammability limits than
    upper flammability limits. Some sources recommend not using it for
    upper flammability limits.
    
    Examples
    --------
    Sample problems from [1]_ for the lower and upper flammability limit.
    
    >>> fire_mixing(ys=normalize([0.0024, 0.0061, 0.0015]), FLs=[.012, .053, .031])
    0.02751172136637642
    
    >>> fire_mixing(ys=normalize([0.0024, 0.0061, 0.0015]), FLs=[.075, .15, .32])
    0.12927551844869378

    References
    ----------
    .. [1] Crowl, Daniel A., and Joseph F. Louvar. Chemical Process Safety:
       Fundamentals with Applications. 2E. Upper Saddle River, N.J: Prentice 
       Hall, 2001.
    '''
    tot = 0.0
    for i in range(len(ys)):
        tot += ys[i]/FLs[i]
    return 1.0/tot

def Suzuki_LFL(Hc):
    r'''Calculates lower flammability limit, using the Suzuki [1]_ correlation.
    Uses heat of combustion only.
    
    The lower flammability limit of a gas is air is:
        
    .. math::
        \text{LFL} = \frac{-3.42}{\Delta H_c^{\circ}} + 0.569
        \Delta H_c^{\circ} + 0.0538\Delta H_c^{\circ 2} + 1.80
    
    Parameters
    ----------
    Hc : float
        Heat of combustion of gas [J/mol]
    
    Returns
    -------
    LFL : float
        Lower flammability limit, mole fraction [-]
    
    Notes
    -----
    Fit performed with 112 compounds, r^2 was 0.977.
    LFL in percent volume in air. Hc is at standard conditions, in MJ/mol.
    11 compounds left out as they were outliers.
    Equation does not apply for molecules with halogen atoms, only hydrocarbons
    with oxygen or nitrogen or sulfur.
    No sample calculation provided with the article. However, the equation is
    straightforward.
    Limits of equations's validity are -6135596 J where it predicts a
    LFL of 0, and -48322129 J where it predicts a LFL of 1.
    
    Examples
    --------
    Pentane, 1.5 % LFL in literature
    
    >>> Suzuki_LFL(-3536600)
    0.014276107095811815
    
    References
    ----------
    .. [1] Suzuki, Takahiro. "Note: Empirical Relationship between Lower
       Flammability Limits and Standard Enthalpies of Combustion of Organic
       Compounds." Fire and Materials 18, no. 5 (September 1, 1994): 333-36.
       doi:10.1002/fam.810180509.
    '''
    Hc = Hc/1E6
    LFL = -3.42/Hc + 0.569*Hc + 0.0538*Hc*Hc + 1.80
    return LFL/100.


def Suzuki_UFL(Hc):
    r'''Calculates upper flammability limit, using the Suzuki [1]_ correlation.
    Uses heat of combustion only.
    The upper flammability limit of a gas is air is:
        
    .. math::
        \text{UFL} = 6.3\Delta H_c^\circ + 0.567\Delta H_c^{\circ 2} + 23.5
        
    Parameters
    ----------
    Hc : float
        Heat of combustion of gas [J/mol]
        
    Returns
    -------
    UFL : float
        Upper flammability limit, mole fraction
        
    Notes
    -----
    UFL in percent volume in air according to original equation.
    Hc is at standard conditions in the equation, in units of MJ/mol.
    AAPD = 1.2% for 95 compounds used in fit.
    Somewhat better results than the High and Danner method.
    4.9% < UFL < 23.0%
    -890.3 kJ/mol < dHc < -6380 kJ/mol
    r^2 = 0.989
    Sample calculations provided for all chemicals, both this method and
    High and Danner. Examples are from the article.
    Predicts a UFL of 1 at 7320190 J and a UFL of 0 at -5554160 J.
    
    Examples
    --------
    Pentane, literature 7.8% UFL
    
    >>> Suzuki_UFL(-3536600)
    0.0831119493052
    
    References
    ----------
    .. [1] Suzuki, Takahiro, and Kozo Koide. "Short Communication: Correlation
       between Upper Flammability Limits and Thermochemical Properties of
       Organic Compounds." Fire and Materials 18, no. 6 (November 1, 1994):
       393-97. doi:10.1002/fam.810180608.
    '''
    Hc = Hc/1E6
    UFL = 6.3*Hc + 0.567*Hc*Hc + 23.5
    return UFL/100.


def Crowl_Louvar_LFL(atoms):
    r'''Calculates lower flammability limit, using the Crowl-Louvar [1]_
    correlation. Uses molecular formula only.
    The lower flammability limit of a gas is air is:
        
    .. math::
        C_mH_xO_y + zO_2 \to mCO_2 + \frac{x}{2}H_2O
        \text{LFL} = \frac{0.55}{4.76m + 1.19x - 2.38y + 1}
        
    Parameters
    ----------
    atoms : dict
        Dictionary of atoms and atom counts
        
    Returns
    -------
    LFL : float
        Lower flammability limit, mole fraction
        
    Notes
    -----
    Coefficient of 0.55 taken from [2]_
    
    Examples
    --------
    Hexane, example from [1]_, lit. 1.2 %
    
    >>> Crowl_Louvar_LFL({'H': 14, 'C': 6})
    0.011899610558199915
    
    References
    ----------
    .. [1] Crowl, Daniel A., and Joseph F. Louvar. Chemical Process Safety:
       Fundamentals with Applications. 2E. Upper Saddle River, N.J:
       Prentice Hall, 2001.
    .. [2] Jones, G. W. "Inflammation Limits and Their Practical Application
       in Hazardous Industrial Operations." Chemical Reviews 22, no. 1
       (February 1, 1938): 1-26. doi:10.1021/cr60071a001
    '''
    nC, nH, nO = 0, 0, 0
    if 'C' in atoms and atoms['C']:
        nC = atoms['C']
    else:
        return None
    if 'H' in atoms:
        nH = atoms['H']
    if 'O' in atoms:
        nO = atoms['O']
    return 0.55/(4.76*nC + 1.19*nH - 2.38*nO + 1.)


def Crowl_Louvar_UFL(atoms):
    r'''Calculates upper flammability limit, using the Crowl-Louvar [1]_
    correlation. Uses molecular formula only.
    The upper flammability limit of a gas is air is:
        
    .. math::
        C_mH_xO_y + zO_2 \to mCO_2 + \frac{x}{2}H_2O
        \text{UFL} = \frac{3.5}{4.76m + 1.19x - 2.38y + 1}
        
    Parameters
    ----------
    atoms : dict
        Dictionary of atoms and atom counts
        
    Returns
    -------
    UFL : float
        Upper flammability limit, mole fraction
        
    Notes
    -----
    Coefficient of 3.5 taken from [2]_
    
    Examples
    --------
    Hexane, example from [1]_, lit. 7.5 %
    
    >>> Crowl_Louvar_UFL({'H': 14, 'C': 6})
    0.07572479446127219
    
    References
    ----------
    .. [1] Crowl, Daniel A., and Joseph F. Louvar. Chemical Process Safety:
       Fundamentals with Applications. 2E. Upper Saddle River, N.J:
       Prentice Hall, 2001.
    .. [2] Jones, G. W. "Inflammation Limits and Their Practical Application
       in Hazardous Industrial Operations." Chemical Reviews 22, no. 1
       (February 1, 1938): 1-26. doi:10.1021/cr60071a001
    '''
    nC, nH, nO = 0, 0, 0
    if 'C' in atoms and atoms['C']:
        nC = atoms['C']
    else:
        return None
    if 'H' in atoms:
        nH = atoms['H']
    if 'O' in atoms:
        nO = atoms['O']
    return 3.5/(4.76*nC + 1.19*nH - 2.38*nO + 1.)

def NFPA_30_classification(T_flash, Tb=None, Psat_100F=None):
    r'''Classify a chemical's flammability/combustibility according
    to the NFPA 30 standard Flammable and Combustible Liquids Code.

    Class IA: Flash Point < 73°F; Boiling Point < 100°F
    Class IB: Flash Point < 73°F; 100°F <= Boiling Point
    Class IC: 73°F <= Flash Point < 100°F
    Class II: 100°F <= Flash Point < 140°F
    Class IIIA: 140°F <= Flash Point < 200°F
    Class IIIB: 200°F <= Flash Point
    
    Class I liquids are designated as flammable; class II and II 
    liquids are designated as combustible.

    Parameters
    ----------
    T_flash : float
        Flash point (closed‐cup method, adjusted for sea level), [K]
    Tb : float, optional
        Normal boiling point (needed to classify IA and IB liquids), [K]
    Psat_100F : float, optional
        Vapor pressure at 100°F (needed to classify IA and IB liquids), [K]

    Returns
    -------
    classification : str
        One of 'IA', 'IB', 'IC', 'II', 'IIIA', 'IIIB', [-]

    Notes
    -----
    Only one of `Tb` or `Psat_100F` is needed.
    
    Class 'IA' also includes unstable liquids.

    Examples
    --------
    Ethylene oxide
    
    >>> NFPA_30_classification(253.15, 283.55)
    'IA'
    
    Butyl alcohol
    
    >>> NFPA_30_classification(308.15)
    'IC'
    
    References
    ----------
    .. [1] NFPA (National Fire Prevention Association). NFPA 30: Flammable and 
       Combustible Liquids Code, 2008. National Fire Protection Association
       (NFPA), 2007.
    '''
    F_100 = 310.92777777777777
    F_73 = 295.92777777777775
    F_140 = 333.15
    F_200 = 366.4833333333333
    Tb_above_100F = False # For numba only
    if Tb is not None:
        Tb_above_100F = Tb >= F_100
    elif Psat_100F is not None:
        Tb_above_100F = Psat_100F < 101325.0
    elif T_flash < F_73:
        raise ValueError("Tb or Psat_100F is required to classify the provided inputs")
    
    if T_flash < F_100:
        if T_flash < F_73 and not Tb_above_100F:
            # Also unstable flammable liquids
            # ethylene oxide, methyl chloride, pentane should go here
            return 'IA'
        elif T_flash < F_73 and Tb_above_100F:
            # acetone, benzene, ethyl alcohol, and isopropyl alcohol.
            return 'IB'
        elif F_73 <= T_flash < F_100:
            # butyl alcohol, diethyl glycol, styrene, and turpentine.
            return 'IC'
    if F_100 <= T_flash < F_140:
        return 'II'
    if F_140 <= T_flash < F_200:
        return 'IIIA'
    if F_200 <= T_flash:
        return 'IIIB'
