From 4106219cd3e646e1f20f82b460a7ca0701f44be3 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 4 Sep 2021 18:45:54 -0400 Subject: [PATCH 001/109] Add reaction library to test example for liquidSurfaceReactor --- examples/rmg/liquid_cat/input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rmg/liquid_cat/input.py b/examples/rmg/liquid_cat/input.py index de70d0f47b..36ffd61991 100644 --- a/examples/rmg/liquid_cat/input.py +++ b/examples/rmg/liquid_cat/input.py @@ -1,7 +1,7 @@ # Data sources database( thermoLibraries=['surfaceThermoPt111', 'primaryThermoLibrary', 'thermo_DFT_CCSDTF12_BAC','DFT_QCI_thermo'], # 'surfaceThermoPt' is the default. Thermo data is derived using bindingEnergies for other metals - reactionLibraries = [], + reactionLibraries = [('Surface/CPOX_Pt/Deutschmann2006_adjusted', False)], # when Ni is used change the library to Surface/Deutschmann_Ni seedMechanisms = [], kineticsDepositories = ['training'], kineticsFamilies = ['surface','default'], From be8b9a780183d6a9f319a014b1fc196733d32c7a Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 12:43:25 -0400 Subject: [PATCH 002/109] added Faraday's Constant and vacuum permittivity to Constants --- rmgpy/constants.pxd | 2 +- rmgpy/constants.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/rmgpy/constants.pxd b/rmgpy/constants.pxd index c6e7594c08..50edea8ad8 100644 --- a/rmgpy/constants.pxd +++ b/rmgpy/constants.pxd @@ -25,4 +25,4 @@ # # ############################################################################### -cdef double pi, Na, kB, R, h, hbar, c, e, m_e, m_p, m_n, amu, a0, E_h +cdef double pi, Na, kB, R, h, hbar, c, e, m_e, m_p, m_n, amu, a0, E_h, F diff --git a/rmgpy/constants.py b/rmgpy/constants.py index 43e16b4410..3b546d97d6 100644 --- a/rmgpy/constants.py +++ b/rmgpy/constants.py @@ -110,6 +110,12 @@ #: :math:`\pi = 3.14159 \ldots` pi = float(math.pi) +#: Faradays Constant F in C/mol +F = 96485.3321233100184 + +#: Vacuum permittivity +epsilon_0 = 8.8541878128 + ################################################################################ # Cython does not automatically place module-level variables into the module @@ -130,4 +136,6 @@ 'm_n': m_n, 'm_p': m_p, 'pi': pi, + 'F': F, + 'epsilon_0': epsilon_0, }) From b79efe8079dda1720211af44a2dec52853af410b Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 12:45:00 -0400 Subject: [PATCH 003/109] added Potential as `V` quantity --- rmgpy/quantity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rmgpy/quantity.py b/rmgpy/quantity.py index 149031d13b..5d73546d45 100644 --- a/rmgpy/quantity.py +++ b/rmgpy/quantity.py @@ -776,6 +776,8 @@ def __call__(self, *args, **kwargs): Momentum = UnitType('kg*m/s^2') +Potential = UnitType('V') + Power = UnitType('W') Pressure = UnitType('Pa', common_units=['bar', 'atm', 'torr', 'psi', 'mbar']) From 900d9e69ec4401b2ea36242570d5908a98184cb4 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 12:53:11 -0400 Subject: [PATCH 004/109] added `H+` and `e` atomtypes and increment/decrement charge attributes --- rmgpy/molecule/adjlist.py | 2 +- rmgpy/molecule/atomtype.pxd | 2 + rmgpy/molecule/atomtype.py | 315 +++++++++++++++++++---------------- rmgpy/molecule/draw.py | 20 ++- rmgpy/molecule/element.py | 11 +- rmgpy/molecule/group.py | 49 ++++-- rmgpy/molecule/molecule.py | 2 +- rmgpy/molecule/translator.py | 15 +- 8 files changed, 235 insertions(+), 181 deletions(-) diff --git a/rmgpy/molecule/adjlist.py b/rmgpy/molecule/adjlist.py index 1ebe0ff110..c75b13086b 100644 --- a/rmgpy/molecule/adjlist.py +++ b/rmgpy/molecule/adjlist.py @@ -92,7 +92,7 @@ def check_partial_charge(atom): the theoretical one: """ - if atom.symbol in {'X','L','R'}: + if atom.symbol in {'X','L','R','e','H+'}: return # because we can't check it. valence = PeriodicSystem.valence_electrons[atom.symbol] diff --git a/rmgpy/molecule/atomtype.pxd b/rmgpy/molecule/atomtype.pxd index 573e761645..dbd168c4a0 100644 --- a/rmgpy/molecule/atomtype.pxd +++ b/rmgpy/molecule/atomtype.pxd @@ -39,6 +39,8 @@ cdef class AtomType: cdef public list decrement_radical cdef public list increment_lone_pair cdef public list decrement_lone_pair + cdef public list increment_charge + cdef public list decrement_charge cdef public list single cdef public list all_double diff --git a/rmgpy/molecule/atomtype.py b/rmgpy/molecule/atomtype.py index 7ee6a1fe98..5bddd30a31 100644 --- a/rmgpy/molecule/atomtype.py +++ b/rmgpy/molecule/atomtype.py @@ -30,7 +30,7 @@ """ This module defines the atom types that are available for representing molecular functional groups and substructure patterns. Each available atom type -is defined as an instance of the :class:`AtomType` class. The atom types +is defined as an instance of the :class:`AtomType` class. The atom types themselves are available in the ``ATOMTYPES`` module-level variable, or as the return value from the :meth:`get_atomtype()` method. @@ -65,6 +65,8 @@ class AtomType: `break_bond` ``list`` The atom type(s) that result when an existing single bond to this atom type is broken `increment_radical` ``list`` The atom type(s) that result when the number of radical electrons is incremented `decrement_radical` ``list`` The atom type(s) that result when the number of radical electrons is decremented + `increment_charge` ``list`` The atom type(s) that result when the number of radical electrons is decremented and charge is incremented + `decrement_charge` ``list`` The atom type(s) that result when the number of radical electrons is incremented and charge is decremented `increment_lone_pair` ``list`` The atom type(s) that result when the number of lone electron pairs is incremented `decrement_lone_pair` ``list`` The atom type(s) that result when the number of lone electron pairs is decremented @@ -106,6 +108,8 @@ def __init__(self, label='', self.break_bond = [] self.increment_radical = [] self.decrement_radical = [] + self.increment_charge = [] + self.decrement_charge = [] self.increment_lone_pair = [] self.decrement_lone_pair = [] self.single = single or [] @@ -136,6 +140,8 @@ def __reduce__(self): 'break_bond': self.break_bond, 'increment_radical': self.increment_radical, 'decrement_radical': self.decrement_radical, + 'increment_charge': self.increment_charge, + 'decrement_charge': self.decrement_charge, 'increment_lone_pair': self.increment_lone_pair, 'decrement_lone_pair': self.decrement_lone_pair, 'single': self.single, @@ -164,6 +170,8 @@ def __setstate__(self, d): self.break_bond = d['break_bond'] self.increment_radical = d['increment_radical'] self.decrement_radical = d['decrement_radical'] + self.increment_charge = d['increment_charge'] + self.decrement_charge = d['decrement_charge'] self.increment_lone_pair = d['increment_lone_pair'] self.decrement_lone_pair = d['decrement_lone_pair'] self.single = d['single'] @@ -178,7 +186,7 @@ def __setstate__(self, d): self.charge = d['charge'] def set_actions(self, increment_bond, decrement_bond, form_bond, break_bond, increment_radical, decrement_radical, - increment_lone_pair, decrement_lone_pair): + increment_lone_pair, decrement_lone_pair, increment_charge, decrement_charge): self.increment_bond = increment_bond self.decrement_bond = decrement_bond self.form_bond = form_bond @@ -187,6 +195,8 @@ def set_actions(self, increment_bond, decrement_bond, form_bond, break_bond, inc self.decrement_radical = decrement_radical self.increment_lone_pair = increment_lone_pair self.decrement_lone_pair = decrement_lone_pair + self.increment_charge = increment_charge + self.decrement_charge = decrement_charge def equivalent(self, other): """ @@ -202,7 +212,7 @@ def is_specific_case_of(self, other): atom type `atomType2` or ``False`` otherwise. """ return self is other or self in other.specific - + def get_features(self): """ Returns a list of the features that are checked to determine atomtype @@ -240,6 +250,9 @@ def get_features(self): ATOMTYPES = {} +# Electron +ATOMTYPES['e'] = AtomType(label='e', generic=[], specific=[], lone_pairs=[0], charge=[-1]) + ATOMTYPES['Rx'] = AtomType(label='Rx', generic=[], specific=[ 'H', 'R', @@ -290,7 +303,7 @@ def get_features(self): # Non-surface atomTypes, R being the most generic: ATOMTYPES['R'] = AtomType(label='R', generic=['Rx'], specific=[ - 'H', + 'H','H0','H+', 'R!H', 'R!H!Val7', 'Val4','Val5','Val6','Val7', @@ -349,7 +362,12 @@ def get_features(self): 'I','I1s', 'F','F1s']) -ATOMTYPES['H'] = AtomType('H', generic=['Rx','R'], specific=[]) + +ATOMTYPES['H'] = AtomType('H', generic=['Rx','R'], specific=['H0','H+'], charge=[0,+1]) +ATOMTYPES['H0'] = AtomType('H0', generic=['R','H'], specific=[], single=[0,1], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], + quadruple=[0], benzene=[0], lone_pairs=[0], charge=[0]) +ATOMTYPES['H+'] = AtomType('H+', generic=['R','H'], specific=[], single=[0], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], + quadruple=[0], benzene=[0], lone_pairs=[0], charge=[+1]) ATOMTYPES['He'] = AtomType('He', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[]) ATOMTYPES['Ne'] = AtomType('Ne', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[]) @@ -672,159 +690,162 @@ def get_features(self): ATOMTYPES['Rx'].set_actions(increment_bond=['Rx'], decrement_bond=['Rx'], form_bond=['Rx'], break_bond=['Rx'], increment_radical=['Rx'], decrement_radical=['Rx'], increment_lone_pair=['Rx'], decrement_lone_pair=['Rx']) ATOMTYPES['Rx!H'].set_actions(increment_bond=['Rx!H'], decrement_bond=['Rx!H'], form_bond=['Rx!H'], break_bond=['Rx!H'], increment_radical=['Rx!H'], decrement_radical=['Rx!H'], increment_lone_pair=['Rx!H'], decrement_lone_pair=['Rx!H']) -ATOMTYPES['X'].set_actions(increment_bond=['X'], decrement_bond=['X'], form_bond=['X'], break_bond=['X'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Xv'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Xo'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Xo'].set_actions(increment_bond=['Xo'], decrement_bond=['Xo'], form_bond=[], break_bond=['Xv'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['R'].set_actions(increment_bond=['R'], decrement_bond=['R'], form_bond=['R'], break_bond=['R'], increment_radical=['R'], decrement_radical=['R'], increment_lone_pair=['R'], decrement_lone_pair=['R']) -ATOMTYPES['R!H'].set_actions(increment_bond=['R!H'], decrement_bond=['R!H'], form_bond=['R!H'], break_bond=['R!H'], increment_radical=['R!H'], decrement_radical=['R!H'], increment_lone_pair=['R!H'], decrement_lone_pair=['R!H']) -ATOMTYPES['R!H!Val7'].set_actions(increment_bond=['R!H!Val7'], decrement_bond=['R!H!Val7'], form_bond=['R!H!Val7'], break_bond=['R!H!Val7'], increment_radical=['R!H!Val7'], decrement_radical=['R!H!Val7'], increment_lone_pair=['R!H!Val7'], decrement_lone_pair=['R!H!Val7']) -ATOMTYPES['Val4'].set_actions(increment_bond=['Val4'], decrement_bond=['Val4'], form_bond=['Val4'], break_bond=['Val4'], increment_radical=['Val4'], decrement_radical=['Val4'], increment_lone_pair=['Val4'], decrement_lone_pair=['Val4']) -ATOMTYPES['Val5'].set_actions(increment_bond=['Val5'], decrement_bond=['Val5'], form_bond=['Val5'], break_bond=['Val5'], increment_radical=['Val5'], decrement_radical=['Val5'], increment_lone_pair=['Val5'], decrement_lone_pair=['Val5']) -ATOMTYPES['Val6'].set_actions(increment_bond=['Val6'], decrement_bond=['Val6'], form_bond=['Val6'], break_bond=['Val6'], increment_radical=['Val6'], decrement_radical=['Val6'], increment_lone_pair=['Val6'], decrement_lone_pair=['Val6']) -ATOMTYPES['Val7'].set_actions(increment_bond=['Val7'], decrement_bond=['Val7'], form_bond=['Val7'], break_bond=['Val7'], increment_radical=['Val7'], decrement_radical=['Val7'], increment_lone_pair=['Val7'], decrement_lone_pair=['Val7']) - -ATOMTYPES['H'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['H'], break_bond=['H'], increment_radical=['H'], decrement_radical=['H'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['He'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['He'], decrement_radical=['He'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Ar'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['C'].set_actions(increment_bond=['C'], decrement_bond=['C'], form_bond=['C'], break_bond=['C'], increment_radical=['C'], decrement_radical=['C'], increment_lone_pair=['C'], decrement_lone_pair=['C']) -ATOMTYPES['Ca'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['C2s']) -ATOMTYPES['Cs'].set_actions(increment_bond=['Cd', 'CO', 'CS'], decrement_bond=[], form_bond=['Cs', 'Csc'], break_bond=['Cs'], increment_radical=['Cs'], decrement_radical=['Cs'], increment_lone_pair=['C2s'], decrement_lone_pair=['C2s']) -ATOMTYPES['Csc'].set_actions(increment_bond=['Cdc'], decrement_bond=[], form_bond=['Csc'], break_bond=['Csc', 'Cs'], increment_radical=['Csc'], decrement_radical=['Csc'], increment_lone_pair=['C2sc'], decrement_lone_pair=['C2sc']) -ATOMTYPES['Cd'].set_actions(increment_bond=['Cdd', 'Ct', 'C2tc'], decrement_bond=['Cs'], form_bond=['Cd', 'Cdc'], break_bond=['Cd'], increment_radical=['Cd'], decrement_radical=['Cd'], increment_lone_pair=['C2d'], decrement_lone_pair=[]) -ATOMTYPES['Cdc'].set_actions(increment_bond=['Ctc'], decrement_bond=['Csc'], form_bond=['Cdc'], break_bond=['Cdc', 'Cd', 'CO', 'CS'], increment_radical=['Cdc'], decrement_radical=['Cdc'], increment_lone_pair=['C2dc'], decrement_lone_pair=[]) -ATOMTYPES['CO'].set_actions(increment_bond=['Cdd', 'C2tc'], decrement_bond=['Cs'], form_bond=['CO', 'Cdc'], break_bond=['CO'], increment_radical=['CO'], decrement_radical=['CO'], increment_lone_pair=['C2d'], decrement_lone_pair=[]) -ATOMTYPES['CS'].set_actions(increment_bond=['Cdd', 'C2tc'], decrement_bond=['Cs'], form_bond=['CS', 'Cdc'], break_bond=['CS'], increment_radical=['CS'], decrement_radical=['CS'], increment_lone_pair=['C2d'], decrement_lone_pair=[]) -ATOMTYPES['Cdd'].set_actions(increment_bond=[], decrement_bond=['Cd', 'CO', 'CS'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Ct'].set_actions(increment_bond=['Cq'], decrement_bond=['Cd', 'CO', 'CS'], form_bond=['Ct'], break_bond=['Ct'], increment_radical=['Ct'], decrement_radical=['Ct'], increment_lone_pair=['C2tc'], decrement_lone_pair=[]) -ATOMTYPES['Ctc'].set_actions(increment_bond=[], decrement_bond=['Cdc'], form_bond=['Ct'], break_bond=[], increment_radical=['Ctc'], decrement_radical=['Ctc'], increment_lone_pair=['C2tc'], decrement_lone_pair=[]) -ATOMTYPES['Cb'].set_actions(increment_bond=['Cbf'], decrement_bond=[], form_bond=['Cb'], break_bond=['Cb'], increment_radical=['Cb'], decrement_radical=['Cb'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Cbf'].set_actions(increment_bond=[], decrement_bond=['Cb'], form_bond=[], break_bond=['Cb'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['C2s'].set_actions(increment_bond=['C2d'], decrement_bond=[], form_bond=['C2s'], break_bond=['C2s'], increment_radical=['C2s'], decrement_radical=['C2s'], increment_lone_pair=['Ca'], decrement_lone_pair=['Cs']) -ATOMTYPES['C2sc'].set_actions(increment_bond=['C2dc'], decrement_bond=[], form_bond=['C2sc'], break_bond=['C2sc'], increment_radical=['C2sc'], decrement_radical=['C2sc'], increment_lone_pair=[], decrement_lone_pair=['Cs']) -ATOMTYPES['C2d'].set_actions(increment_bond=['C2tc'], decrement_bond=['C2s'], form_bond=['C2dc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Cd', 'CO', 'CS']) -ATOMTYPES['C2dc'].set_actions(increment_bond=[], decrement_bond=['C2sc'], form_bond=[], break_bond=['C2d'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Cdc']) -ATOMTYPES['C2tc'].set_actions(increment_bond=[], decrement_bond=['C2d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Ct','Ctc']) -ATOMTYPES['Cq'].set_actions(increment_bond=[], decrement_bond=['Ct'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['N'].set_actions(increment_bond=['N'], decrement_bond=['N'], form_bond=['N'], break_bond=['N'], increment_radical=['N'], decrement_radical=['N'], increment_lone_pair=['N'], decrement_lone_pair=['N']) -ATOMTYPES['N0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['N0sc'], break_bond=['N0sc'], increment_radical=['N0sc'], decrement_radical=['N0sc'], increment_lone_pair=[], decrement_lone_pair=['N1s', 'N1sc']) -ATOMTYPES['N1s'].set_actions(increment_bond=['N1dc'], decrement_bond=[], form_bond=['N1s'], break_bond=['N1s'], increment_radical=['N1s'], decrement_radical=['N1s'], increment_lone_pair=['N0sc'], decrement_lone_pair=['N3s', 'N3sc']) -ATOMTYPES['N1sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['N1sc'], break_bond=['N1sc'], increment_radical=['N1sc'], decrement_radical=['N1sc'], increment_lone_pair=[], decrement_lone_pair=['N3s', 'N3sc']) -ATOMTYPES['N1dc'].set_actions(increment_bond=['N1dc'], decrement_bond=['N1s', 'N1dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['N3d']) -ATOMTYPES['N3s'].set_actions(increment_bond=['N3d'], decrement_bond=[], form_bond=['N3s'], break_bond=['N3s'], increment_radical=['N3s'], decrement_radical=['N3s'], increment_lone_pair=['N1s', 'N1sc'], decrement_lone_pair=['N5sc']) -ATOMTYPES['N3sc'].set_actions(increment_bond=['N3d'], decrement_bond=[], form_bond=['N3sc'], break_bond=['N3sc'], increment_radical=['N3sc'], decrement_radical=['N3sc'], increment_lone_pair=['N1s', 'N1sc'], decrement_lone_pair=['N5sc']) -ATOMTYPES['N3d'].set_actions(increment_bond=['N3t'], decrement_bond=['N3s', 'N3sc'], form_bond=['N3d'], break_bond=['N3d'], increment_radical=['N3d'], decrement_radical=['N3d'], increment_lone_pair=['N1dc'], decrement_lone_pair=['N5dc']) -ATOMTYPES['N3t'].set_actions(increment_bond=[], decrement_bond=['N3d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N3b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5sc'].set_actions(increment_bond=['N5dc'], decrement_bond=[], form_bond=['N5sc'], break_bond=['N5sc'], increment_radical=['N5sc'], decrement_radical=['N5sc'], increment_lone_pair=['N3s', 'N3sc'], decrement_lone_pair=[]) -ATOMTYPES['N5dc'].set_actions(increment_bond=['N5ddc', 'N5tc'], decrement_bond=['N5sc'], form_bond=['N5dc'], break_bond=['N5dc'], increment_radical=['N5dc'], decrement_radical=['N5dc'], increment_lone_pair=['N3d'], decrement_lone_pair=[]) -ATOMTYPES['N5ddc'].set_actions(increment_bond=['N5dddc'], decrement_bond=['N5dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5dddc'].set_actions(increment_bond=[], decrement_bond=['N5ddc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5tc'].set_actions(increment_bond=[], decrement_bond=['N5dc'], form_bond=['N5tc'], break_bond=['N5tc'], increment_radical=['N5tc'], decrement_radical=['N5tc'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5b'].set_actions(increment_bond=['N5bd'], decrement_bond=[], form_bond=['N5b'], break_bond=['N5b'], increment_radical=['N5b'], decrement_radical=['N5b'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5bd'].set_actions(increment_bond=[], decrement_bond=['N5b'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['O'].set_actions(increment_bond=['O'], decrement_bond=['O'], form_bond=['O'], break_bond=['O'], increment_radical=['O'], decrement_radical=['O'], increment_lone_pair=['O'], decrement_lone_pair=['O']) -ATOMTYPES['Oa'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['O0sc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['O2s', 'O2sc']) -ATOMTYPES['O0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['O0sc'], break_bond=['Oa', 'O0sc'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['O2s', 'O2sc']) -ATOMTYPES['O2s'].set_actions(increment_bond=['O2d'], decrement_bond=[], form_bond=['O2s', 'O2sc'], break_bond=['O2s'], increment_radical=['O2s'], decrement_radical=['O2s'], increment_lone_pair=['Oa', 'O0sc'], decrement_lone_pair=['O4sc']) -ATOMTYPES['O2sc'].set_actions(increment_bond=['O2d'], decrement_bond=[], form_bond=[], break_bond=['O2s'], increment_radical=['O2sc'], decrement_radical=['O2sc'], increment_lone_pair=[], decrement_lone_pair=['O4sc']) -ATOMTYPES['O2d'].set_actions(increment_bond=[], decrement_bond=['O2s', 'O2sc'], form_bond=[], break_bond=[], increment_radical=['O2d'], decrement_radical=['O2d'], increment_lone_pair=[], decrement_lone_pair=['O4dc', 'O4tc']) -ATOMTYPES['O4sc'].set_actions(increment_bond=['O4dc'], decrement_bond=[], form_bond=['O4sc'], break_bond=['O4sc'], increment_radical=['O4sc'], decrement_radical=['O4sc'], increment_lone_pair=['O2s', 'O2sc'], decrement_lone_pair=[]) -ATOMTYPES['O4dc'].set_actions(increment_bond=['O4tc'], decrement_bond=['O4sc'], form_bond=['O4dc'], break_bond=['O4dc'], increment_radical=['O4dc'], decrement_radical=['O4dc'], increment_lone_pair=['O2d'], decrement_lone_pair=[]) -ATOMTYPES['O4tc'].set_actions(increment_bond=[], decrement_bond=['O4dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['O4b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['Si'].set_actions(increment_bond=['Si'], decrement_bond=['Si'], form_bond=['Si'], break_bond=['Si'], increment_radical=['Si'], decrement_radical=['Si'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sis'].set_actions(increment_bond=['Sid', 'SiO'], decrement_bond=[], form_bond=['Sis'], break_bond=['Sis'], increment_radical=['Sis'], decrement_radical=['Sis'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sid'].set_actions(increment_bond=['Sidd', 'Sit'], decrement_bond=['Sis'], form_bond=['Sid'], break_bond=['Sid'], increment_radical=['Sid'], decrement_radical=['Sid'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sidd'].set_actions(increment_bond=[], decrement_bond=['Sid', 'SiO'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sit'].set_actions(increment_bond=['Siq'], decrement_bond=['Sid'], form_bond=['Sit'], break_bond=['Sit'], increment_radical=['Sit'], decrement_radical=['Sit'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['SiO'].set_actions(increment_bond=['Sidd'], decrement_bond=['Sis'], form_bond=['SiO'], break_bond=['SiO'], increment_radical=['SiO'], decrement_radical=['SiO'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sib'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Sib'], break_bond=['Sib'], increment_radical=['Sib'], decrement_radical=['Sib'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sibf'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Siq'].set_actions(increment_bond=[], decrement_bond=['Sit'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['P'].set_actions(increment_bond=['P'], decrement_bond=['P'], form_bond=['P'], break_bond=['P'], increment_radical=['P'], decrement_radical=['P'], increment_lone_pair=['P'], decrement_lone_pair=['P']) -ATOMTYPES['P0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['P0sc'], break_bond=['P0sc'], increment_radical=['P0sc'], decrement_radical=['P0sc'], increment_lone_pair=[], decrement_lone_pair=['P1s', 'P1sc']) -ATOMTYPES['P1s'].set_actions(increment_bond=['P1dc'], decrement_bond=[], form_bond=['P1s'], break_bond=['P1s'], increment_radical=['P1s'], decrement_radical=['P1s'], increment_lone_pair=['P0sc'], decrement_lone_pair=['P3s']) -ATOMTYPES['P1sc'].set_actions(increment_bond=['P1dc'], decrement_bond=[], form_bond=['P1sc'], break_bond=['P1sc'], increment_radical=['P1sc'], decrement_radical=['P1sc'], increment_lone_pair=['P0sc'], decrement_lone_pair=['P3s']) -ATOMTYPES['P1dc'].set_actions(increment_bond=[], decrement_bond=['P1s'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['P3d']) -ATOMTYPES['P3s'].set_actions(increment_bond=['P3d'], decrement_bond=[], form_bond=['P3s'], break_bond=['P3s'], increment_radical=['P3s'], decrement_radical=['P3s'], increment_lone_pair=['P1s', 'P1sc'], decrement_lone_pair=['P5s', 'P5sc']) -ATOMTYPES['P3d'].set_actions(increment_bond=['P3t'], decrement_bond=['P3s'], form_bond=['P3d'], break_bond=['P3d'], increment_radical=['P3d'], decrement_radical=['P3d'], increment_lone_pair=['P1dc'], decrement_lone_pair=['P5d', 'P5dc']) -ATOMTYPES['P3t'].set_actions(increment_bond=[], decrement_bond=['P3d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['P5t', 'P5tc']) -ATOMTYPES['P3b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5s'].set_actions(increment_bond=['P5d', 'P5dc'], decrement_bond=[], form_bond=['P5s'], break_bond=['P5s'], increment_radical=['P5s'], decrement_radical=['P5s'], increment_lone_pair=['P3s'], decrement_lone_pair=[]) -ATOMTYPES['P5sc'].set_actions(increment_bond=['P5dc'], decrement_bond=[], form_bond=['P5sc'], break_bond=['P5sc'], increment_radical=['P5sc'], decrement_radical=['P5sc'], increment_lone_pair=['P3s'], decrement_lone_pair=[]) -ATOMTYPES['P5d'].set_actions(increment_bond=['P5dd', 'P5ddc', 'P5t', 'P5tc'], decrement_bond=['P5s'], form_bond=['P5d'], break_bond=['P5d'], increment_radical=['P5d'], decrement_radical=['P5d'], increment_lone_pair=['P3d'], decrement_lone_pair=[]) -ATOMTYPES['P5dd'].set_actions(increment_bond=['P5td'], decrement_bond=['P5d', 'P5dc'], form_bond=['P5dd'], break_bond=['P5dd'], increment_radical=['P5dd'], decrement_radical=['P5dd'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5dc'].set_actions(increment_bond=['P5dd', 'P5ddc', 'P5tc'], decrement_bond=['P5sc'], form_bond=['P5dc'], break_bond=['P5dc'], increment_radical=['P5dc'], decrement_radical=['P5dc'], increment_lone_pair=['P3d'], decrement_lone_pair=[]) -ATOMTYPES['P5ddc'].set_actions(increment_bond=[], decrement_bond=['P5dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5t'].set_actions(increment_bond=['P5td'], decrement_bond=['P5d'], form_bond=['P5t'], break_bond=['P5t'], increment_radical=['P5t'], decrement_radical=['P5t'], increment_lone_pair=['P3t'], decrement_lone_pair=[]) -ATOMTYPES['P5td'].set_actions(increment_bond=[], decrement_bond=['P5t', 'P5dd'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5tc'].set_actions(increment_bond=[], decrement_bond=['P5dc'], form_bond=['P5tc'], break_bond=['P5tc'], increment_radical=['P5tc'], decrement_radical=['P5tc'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5b'].set_actions(increment_bond=['P5bd'], decrement_bond=[], form_bond=['P5b'], break_bond=['P5b'], increment_radical=['P5b'], decrement_radical=['P5b'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5bd'].set_actions(increment_bond=[], decrement_bond=['P5b'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['S'].set_actions(increment_bond=['S'], decrement_bond=['S'], form_bond=['S'], break_bond=['S'], increment_radical=['S'], decrement_radical=['S'], increment_lone_pair=['S'], decrement_lone_pair=['S']) -ATOMTYPES['S0sc'].set_actions(increment_bond=['S0sc'], decrement_bond=['S0sc'], form_bond=['S0sc'], break_bond=['Sa', 'S0sc'], increment_radical=['S0sc'], decrement_radical=['S0sc'], increment_lone_pair=[], decrement_lone_pair=['S2s', 'S2sc', 'S2dc', 'S2tc']) -ATOMTYPES['Sa'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['S0sc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['S2s']) -ATOMTYPES['S2s'].set_actions(increment_bond=['S2d', 'S2dc'], decrement_bond=[], form_bond=['S2s', 'S2sc'], break_bond=['S2s'], increment_radical=['S2s'], decrement_radical=['S2s'], increment_lone_pair=['Sa', 'S0sc'], decrement_lone_pair=['S4s', 'S4sc']) -ATOMTYPES['S2sc'].set_actions(increment_bond=['S2dc'], decrement_bond=[], form_bond=['S2sc'], break_bond=['S2sc', 'S2s'], increment_radical=['S2sc'], decrement_radical=['S2sc'], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4s', 'S4sc']) -ATOMTYPES['S2d'].set_actions(increment_bond=['S2tc'], decrement_bond=['S2s'], form_bond=['S2d'], break_bond=['S2d'], increment_radical=['S2d'], decrement_radical=['S2d'], increment_lone_pair=[], decrement_lone_pair=['S4dc', 'S4d']) -ATOMTYPES['S2dc'].set_actions(increment_bond=['S2tc', 'S2dc'], decrement_bond=['S2sc', 'S2s', 'S2dc'], form_bond=['S2dc'], break_bond=['S2dc'], increment_radical=['S2dc'], decrement_radical=['S2dc'], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4d', 'S4dc']) -ATOMTYPES['S2tc'].set_actions(increment_bond=[], decrement_bond=['S2d', 'S2dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4t']) -ATOMTYPES['S4s'].set_actions(increment_bond=['S4d', 'S4dc'], decrement_bond=[], form_bond=['S4s'], break_bond=['S4s'], increment_radical=['S4s'], decrement_radical=['S4s'], increment_lone_pair=['S2s', 'S2sc'], decrement_lone_pair=['S6s']) -ATOMTYPES['S4sc'].set_actions(increment_bond=['S4d', 'S4dc'], decrement_bond=[], form_bond=['S4s', 'S4sc'], break_bond=['S4sc'], increment_radical=['S4sc'], decrement_radical=['S4sc'], increment_lone_pair=['S2s', 'S2sc'], decrement_lone_pair=['S6s']) -ATOMTYPES['S4d'].set_actions(increment_bond=['S4dd', 'S4dc', 'S4t', 'S4tdc'], decrement_bond=['S4s', 'S4sc'], form_bond=['S4dc', 'S4d'], break_bond=['S4d', 'S4dc'], increment_radical=['S4d'], decrement_radical=['S4d'], increment_lone_pair=['S2d', 'S2dc'], decrement_lone_pair=['S6d', 'S6dc']) -ATOMTYPES['S4dc'].set_actions(increment_bond=['S4dd', 'S4dc', 'S4tdc'], decrement_bond=['S4sc', 'S4dc'], form_bond=['S4d', 'S4dc'], break_bond=['S4d', 'S4dc'], increment_radical=['S4dc'], decrement_radical=['S4dc'], increment_lone_pair=['S2d', 'S2dc'], decrement_lone_pair=['S6d', 'S6dc']) -ATOMTYPES['S4b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['S4dd'].set_actions(increment_bond=['S4dc'], decrement_bond=['S4dc', 'S4d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['S6dd']) -ATOMTYPES['S4t'].set_actions(increment_bond=[], decrement_bond=['S4d'], form_bond=['S4t'], break_bond=['S4t'], increment_radical=['S4t'], decrement_radical=['S4t'], increment_lone_pair=['S2tc'], decrement_lone_pair=['S6t', 'S6tdc']) -ATOMTYPES['S4tdc'].set_actions(increment_bond=['S4tdc'], decrement_bond=['S4d', 'S4tdc'], form_bond=['S4tdc'], break_bond=['S4tdc'], increment_radical=['S4tdc'], decrement_radical=['S4tdc'], increment_lone_pair=['S6tdc'], decrement_lone_pair=['S6td', 'S6tdc']) -ATOMTYPES['S6s'].set_actions(increment_bond=['S6d', 'S6dc'], decrement_bond=[], form_bond=['S6s'], break_bond=['S6s'], increment_radical=['S6s'], decrement_radical=['S6s'], increment_lone_pair=['S4s', 'S4sc'], decrement_lone_pair=[]) -ATOMTYPES['S6sc'].set_actions(increment_bond=['S6dc'], decrement_bond=[], form_bond=['S6sc'], break_bond=['S6sc'], increment_radical=['S6sc'], decrement_radical=['S6sc'], increment_lone_pair=['S4s', 'S4sc'], decrement_lone_pair=[]) -ATOMTYPES['S6d'].set_actions(increment_bond=['S6dd', 'S6t', 'S6tdc'], decrement_bond=['S6s'], form_bond=['S6d', 'S6dc'], break_bond=['S6d', 'S6dc'], increment_radical=['S6d'], decrement_radical=['S6d'], increment_lone_pair=['S4d', 'S4dc'], decrement_lone_pair=[]) -ATOMTYPES['S6dc'].set_actions(increment_bond=['S6dd', 'S6ddd', 'S6dc', 'S6t', 'S6td', 'S6tdc'], decrement_bond=['S6sc', 'S6dc'], form_bond=['S6d', 'S6dc'], break_bond=['S6d', 'S6dc'], increment_radical=['S6dc'], decrement_radical=['S6dc'], increment_lone_pair=['S4d', 'S4dc'], decrement_lone_pair=[]) -ATOMTYPES['S6dd'].set_actions(increment_bond=['S6ddd', 'S6td'], decrement_bond=['S6d', 'S6dc'], form_bond=['S6dd', 'S6dc'], break_bond=['S6dd'], increment_radical=['S6dd'], decrement_radical=['S6dd'], increment_lone_pair=['S4dd'], decrement_lone_pair=[]) -ATOMTYPES['S6ddd'].set_actions(increment_bond=[], decrement_bond=['S6dd', 'S6dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['S6t'].set_actions(increment_bond=['S6td'], decrement_bond=['S6d', 'S6dc'], form_bond=['S6t'], break_bond=['S6t'], increment_radical=['S6t'], decrement_radical=['S6t'], increment_lone_pair=['S4t'], decrement_lone_pair=[]) -ATOMTYPES['S6td'].set_actions(increment_bond=['S6tt', 'S6tdc'], decrement_bond=['S6dc', 'S6t', 'S6dd', 'S6tdc'], form_bond=['S6td'], break_bond=['S6td'], increment_radical=['S6td'], decrement_radical=['S6td'], increment_lone_pair=['S4tdc'], decrement_lone_pair=[]) -ATOMTYPES['S6tt'].set_actions(increment_bond=[], decrement_bond=['S6td', 'S6tdc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['S6tdc'].set_actions(increment_bond=['S6td', 'S6tdc', 'S6tt'], decrement_bond=['S6dc', 'S6tdc'], form_bond=['S6tdc'], break_bond=['S6tdc'], increment_radical=['S6tdc'], decrement_radical=['S6tdc'], increment_lone_pair=['S4t', 'S4tdc'], decrement_lone_pair=[]) - -ATOMTYPES['Cl'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Cl'], break_bond=['Cl'], increment_radical=['Cl'], decrement_radical=['Cl'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Cl1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Cl1s'], break_bond=['Cl1s'], increment_radical=['Cl1s'], decrement_radical=['Cl1s'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['Br'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Br'], break_bond=['Br'], increment_radical=['Br'], decrement_radical=['Br'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Br1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Br1s'], break_bond=['Br1s'], increment_radical=['Br1s'], decrement_radical=['Br1s'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['I'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['I'], break_bond=['I'], increment_radical=['I'], decrement_radical=['I'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['I1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['I1s'], break_bond=['I1s'], increment_radical=['I1s'], decrement_radical=['I1s'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['F'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F'], break_bond=['F'], increment_radical=['F'], decrement_radical=['F'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['F1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F1s'], break_bond=['F1s'], increment_radical=['F1s'], decrement_radical=['F1s'], increment_lone_pair=[], decrement_lone_pair=[]) + +ATOMTYPES['e'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['X'].set_actions(increment_bond=['X'], decrement_bond=['X'], form_bond=['X'], break_bond=['X'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Xv'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Xo'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Xo'].set_actions(increment_bond=['Xo'], decrement_bond=['Xo'], form_bond=[], break_bond=['Xv'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['R'].set_actions(increment_bond=['R'], decrement_bond=['R'], form_bond=['R'], break_bond=['R'], increment_radical=['R'], decrement_radical=['R'], increment_lone_pair=['R'], decrement_lone_pair=['R'], increment_charge=['R'], decrement_charge=['R']) +ATOMTYPES['R!H'].set_actions(increment_bond=['R!H'], decrement_bond=['R!H'], form_bond=['R!H'], break_bond=['R!H'], increment_radical=['R!H'], decrement_radical=['R!H'], increment_lone_pair=['R!H'], decrement_lone_pair=['R!H'], increment_charge=['R!H'], decrement_charge=['R!H']) +ATOMTYPES['R!H!Val7'].set_actions(increment_bond=['R!H!Val7'], decrement_bond=['R!H!Val7'], form_bond=['R!H!Val7'], break_bond=['R!H!Val7'], increment_radical=['R!H!Val7'], decrement_radical=['R!H!Val7'], increment_lone_pair=['R!H!Val7'], decrement_lone_pair=['R!H!Val7'], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Val4'].set_actions(increment_bond=['Val4'], decrement_bond=['Val4'], form_bond=['Val4'], break_bond=['Val4'], increment_radical=['Val4'], decrement_radical=['Val4'], increment_lone_pair=['Val4'], decrement_lone_pair=['Val4'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Val5'].set_actions(increment_bond=['Val5'], decrement_bond=['Val5'], form_bond=['Val5'], break_bond=['Val5'], increment_radical=['Val5'], decrement_radical=['Val5'], increment_lone_pair=['Val5'], decrement_lone_pair=['Val5'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Val6'].set_actions(increment_bond=['Val6'], decrement_bond=['Val6'], form_bond=['Val6'], break_bond=['Val6'], increment_radical=['Val6'], decrement_radical=['Val6'], increment_lone_pair=['Val6'], decrement_lone_pair=['Val6'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Val7'].set_actions(increment_bond=['Val7'], decrement_bond=['Val7'], form_bond=['Val7'], break_bond=['Val7'], increment_radical=['Val7'], decrement_radical=['Val7'], increment_lone_pair=['Val7'], decrement_lone_pair=['Val7'],increment_charge=[], decrement_charge=[]) + +ATOMTYPES['H'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['H'], break_bond=['H'], increment_radical=['H'], decrement_radical=['H'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['H'], decrement_charge=['H']) +ATOMTYPES['H0'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['H0'], break_bond=['H0'], increment_radical=['H0'], decrement_radical=['H0'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['H+'], decrement_charge=[]) +ATOMTYPES['H+'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=['H0']) + +ATOMTYPES['He'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['He'], decrement_radical=['He'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Ar'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['C'].set_actions(increment_bond=['C'], decrement_bond=['C'], form_bond=['C'], break_bond=['C'], increment_radical=['C'], decrement_radical=['C'], increment_lone_pair=['C'], decrement_lone_pair=['C'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Ca'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['C2s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cs'].set_actions(increment_bond=['Cd', 'CO', 'CS'], decrement_bond=[], form_bond=['Cs', 'Csc'], break_bond=['Cs'], increment_radical=['Cs'], decrement_radical=['Cs'], increment_lone_pair=['C2s'], decrement_lone_pair=['C2s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Csc'].set_actions(increment_bond=['Cdc'], decrement_bond=[], form_bond=['Csc'], break_bond=['Csc', 'Cs'], increment_radical=['Csc'], decrement_radical=['Csc'], increment_lone_pair=['C2sc'], decrement_lone_pair=['C2sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cd'].set_actions(increment_bond=['Cdd', 'Ct', 'C2tc'], decrement_bond=['Cs'], form_bond=['Cd', 'Cdc'], break_bond=['Cd'], increment_radical=['Cd'], decrement_radical=['Cd'], increment_lone_pair=['C2d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cdc'].set_actions(increment_bond=[], decrement_bond=['Csc'], form_bond=['Cdc'], break_bond=['Cdc', 'Cd', 'CO', 'CS'], increment_radical=['Cdc'], decrement_radical=['Cdc'], increment_lone_pair=['C2dc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['CO'].set_actions(increment_bond=['Cdd', 'C2tc'], decrement_bond=['Cs'], form_bond=['CO', 'Cdc'], break_bond=['CO'], increment_radical=['CO'], decrement_radical=['CO'], increment_lone_pair=['C2d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['CS'].set_actions(increment_bond=['Cdd', 'C2tc'], decrement_bond=['Cs'], form_bond=['CS', 'Cdc'], break_bond=['CS'], increment_radical=['CS'], decrement_radical=['CS'], increment_lone_pair=['C2d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cdd'].set_actions(increment_bond=[], decrement_bond=['Cd', 'CO', 'CS'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Ct'].set_actions(increment_bond=['Cq'], decrement_bond=['Cd', 'CO', 'CS'], form_bond=['Ct'], break_bond=['Ct'], increment_radical=['Ct'], decrement_radical=['Ct'], increment_lone_pair=['C2tc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cb'].set_actions(increment_bond=['Cbf'], decrement_bond=[], form_bond=['Cb'], break_bond=['Cb'], increment_radical=['Cb'], decrement_radical=['Cb'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cbf'].set_actions(increment_bond=[], decrement_bond=['Cb'], form_bond=[], break_bond=['Cb'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2s'].set_actions(increment_bond=['C2d'], decrement_bond=[], form_bond=['C2s'], break_bond=['C2s'], increment_radical=['C2s'], decrement_radical=['C2s'], increment_lone_pair=['Ca'], decrement_lone_pair=['Cs'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2sc'].set_actions(increment_bond=['C2dc'], decrement_bond=[], form_bond=['C2sc'], break_bond=['C2sc'], increment_radical=['C2sc'], decrement_radical=['C2sc'], increment_lone_pair=[], decrement_lone_pair=['Cs'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2d'].set_actions(increment_bond=['C2tc'], decrement_bond=['C2s'], form_bond=['C2dc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Cd', 'CO', 'CS'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2dc'].set_actions(increment_bond=[], decrement_bond=['C2sc'], form_bond=[], break_bond=['C2d'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Cdc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2tc'].set_actions(increment_bond=[], decrement_bond=['C2d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Ct'], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cq'].set_actions(increment_bond=[], decrement_bond=['Ct'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['N'].set_actions(increment_bond=['N'], decrement_bond=['N'], form_bond=['N'], break_bond=['N'], increment_radical=['N'], decrement_radical=['N'], increment_lone_pair=['N'], decrement_lone_pair=['N'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['N0sc'], break_bond=['N0sc'], increment_radical=['N0sc'], decrement_radical=['N0sc'], increment_lone_pair=[], decrement_lone_pair=['N1s', 'N1sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N1s'].set_actions(increment_bond=['N1dc'], decrement_bond=[], form_bond=['N1s'], break_bond=['N1s'], increment_radical=['N1s'], decrement_radical=['N1s'], increment_lone_pair=['N0sc'], decrement_lone_pair=['N3s', 'N3sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N1sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['N1sc'], break_bond=['N1sc'], increment_radical=['N1sc'], decrement_radical=['N1sc'], increment_lone_pair=[], decrement_lone_pair=['N3s', 'N3sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N1dc'].set_actions(increment_bond=['N1dc'], decrement_bond=['N1s', 'N1dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['N3d'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3s'].set_actions(increment_bond=['N3d'], decrement_bond=[], form_bond=['N3s'], break_bond=['N3s'], increment_radical=['N3s'], decrement_radical=['N3s'], increment_lone_pair=['N1s', 'N1sc'], decrement_lone_pair=['N5sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3sc'].set_actions(increment_bond=['N3d'], decrement_bond=[], form_bond=['N3sc'], break_bond=['N3sc'], increment_radical=['N3sc'], decrement_radical=['N3sc'], increment_lone_pair=['N1s', 'N1sc'], decrement_lone_pair=['N5sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3d'].set_actions(increment_bond=['N3t'], decrement_bond=['N3s', 'N3sc'], form_bond=['N3d'], break_bond=['N3d'], increment_radical=['N3d'], decrement_radical=['N3d'], increment_lone_pair=['N1dc'], decrement_lone_pair=['N5dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3t'].set_actions(increment_bond=[], decrement_bond=['N3d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5sc'].set_actions(increment_bond=['N5dc'], decrement_bond=[], form_bond=['N5sc'], break_bond=['N5sc'], increment_radical=['N5sc'], decrement_radical=['N5sc'], increment_lone_pair=['N3s', 'N3sc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5dc'].set_actions(increment_bond=['N5ddc', 'N5tc'], decrement_bond=['N5sc'], form_bond=['N5dc'], break_bond=['N5dc'], increment_radical=['N5dc'], decrement_radical=['N5dc'], increment_lone_pair=['N3d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5ddc'].set_actions(increment_bond=['N5dddc'], decrement_bond=['N5dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5dddc'].set_actions(increment_bond=[], decrement_bond=['N5ddc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5tc'].set_actions(increment_bond=[], decrement_bond=['N5dc'], form_bond=['N5tc'], break_bond=['N5tc'], increment_radical=['N5tc'], decrement_radical=['N5tc'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5b'].set_actions(increment_bond=['N5bd'], decrement_bond=[], form_bond=['N5b'], break_bond=['N5b'], increment_radical=['N5b'], decrement_radical=['N5b'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5bd'].set_actions(increment_bond=[], decrement_bond=['N5b'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['O'].set_actions(increment_bond=['O'], decrement_bond=['O'], form_bond=['O'], break_bond=['O'], increment_radical=['O'], decrement_radical=['O'], increment_lone_pair=['O'], decrement_lone_pair=['O'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Oa'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['O0sc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['O2s', 'O2sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['O0sc'], break_bond=['Oa', 'O0sc'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['O2s', 'O2sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O2s'].set_actions(increment_bond=['O2d'], decrement_bond=[], form_bond=['O2s', 'O2sc'], break_bond=['O2s'], increment_radical=['O2s'], decrement_radical=['O2s'], increment_lone_pair=['Oa', 'O0sc'], decrement_lone_pair=['O4sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O2sc'].set_actions(increment_bond=['O2d'], decrement_bond=[], form_bond=[], break_bond=['O2s'], increment_radical=['O2sc'], decrement_radical=['O2sc'], increment_lone_pair=[], decrement_lone_pair=['O4sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O2d'].set_actions(increment_bond=[], decrement_bond=['O2s', 'O2sc'], form_bond=[], break_bond=[], increment_radical=['O2d'], decrement_radical=['O2d'], increment_lone_pair=[], decrement_lone_pair=['O4dc', 'O4tc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O4sc'].set_actions(increment_bond=['O4dc'], decrement_bond=[], form_bond=['O4sc'], break_bond=['O4sc'], increment_radical=['O4sc'], decrement_radical=['O4sc'], increment_lone_pair=['O2s', 'O2sc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['O4dc'].set_actions(increment_bond=['O4tc'], decrement_bond=['O4sc'], form_bond=['O4dc'], break_bond=['O4dc'], increment_radical=['O4dc'], decrement_radical=['O4dc'], increment_lone_pair=['O2d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['O4tc'].set_actions(increment_bond=[], decrement_bond=['O4dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['O4b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['Si'].set_actions(increment_bond=['Si'], decrement_bond=['Si'], form_bond=['Si'], break_bond=['Si'], increment_radical=['Si'], decrement_radical=['Si'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sis'].set_actions(increment_bond=['Sid', 'SiO'], decrement_bond=[], form_bond=['Sis'], break_bond=['Sis'], increment_radical=['Sis'], decrement_radical=['Sis'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sid'].set_actions(increment_bond=['Sidd', 'Sit'], decrement_bond=['Sis'], form_bond=['Sid'], break_bond=['Sid'], increment_radical=['Sid'], decrement_radical=['Sid'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sidd'].set_actions(increment_bond=[], decrement_bond=['Sid', 'SiO'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sit'].set_actions(increment_bond=['Siq'], decrement_bond=['Sid'], form_bond=['Sit'], break_bond=['Sit'], increment_radical=['Sit'], decrement_radical=['Sit'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['SiO'].set_actions(increment_bond=['Sidd'], decrement_bond=['Sis'], form_bond=['SiO'], break_bond=['SiO'], increment_radical=['SiO'], decrement_radical=['SiO'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sib'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Sib'], break_bond=['Sib'], increment_radical=['Sib'], decrement_radical=['Sib'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sibf'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Siq'].set_actions(increment_bond=[], decrement_bond=['Sit'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['P'].set_actions(increment_bond=['P'], decrement_bond=['P'], form_bond=['P'], break_bond=['P'], increment_radical=['P'], decrement_radical=['P'], increment_lone_pair=['P'], decrement_lone_pair=['P'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['P0sc'], break_bond=['P0sc'], increment_radical=['P0sc'], decrement_radical=['P0sc'], increment_lone_pair=[], decrement_lone_pair=['P1s', 'P1sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P1s'].set_actions(increment_bond=['P1dc'], decrement_bond=[], form_bond=['P1s'], break_bond=['P1s'], increment_radical=['P1s'], decrement_radical=['P1s'], increment_lone_pair=['P0sc'], decrement_lone_pair=['P3s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P1sc'].set_actions(increment_bond=['P1dc'], decrement_bond=[], form_bond=['P1sc'], break_bond=['P1sc'], increment_radical=['P1sc'], decrement_radical=['P1sc'], increment_lone_pair=['P0sc'], decrement_lone_pair=['P3s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P1dc'].set_actions(increment_bond=[], decrement_bond=['P1s'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['P3d'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P3s'].set_actions(increment_bond=['P3d'], decrement_bond=[], form_bond=['P3s'], break_bond=['P3s'], increment_radical=['P3s'], decrement_radical=['P3s'], increment_lone_pair=['P1s', 'P1sc'], decrement_lone_pair=['P5s', 'P5sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P3d'].set_actions(increment_bond=['P3t'], decrement_bond=['P3s'], form_bond=['P3d'], break_bond=['P3d'], increment_radical=['P3d'], decrement_radical=['P3d'], increment_lone_pair=['P1dc'], decrement_lone_pair=['P5d', 'P5dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P3t'].set_actions(increment_bond=[], decrement_bond=['P3d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['P5t', 'P5tc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P3b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5s'].set_actions(increment_bond=['P5d', 'P5dc'], decrement_bond=[], form_bond=['P5s'], break_bond=['P5s'], increment_radical=['P5s'], decrement_radical=['P5s'], increment_lone_pair=['P3s'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5sc'].set_actions(increment_bond=['P5dc'], decrement_bond=[], form_bond=['P5sc'], break_bond=['P5sc'], increment_radical=['P5sc'], decrement_radical=['P5sc'], increment_lone_pair=['P3s'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5d'].set_actions(increment_bond=['P5dd', 'P5ddc', 'P5t', 'P5tc'], decrement_bond=['P5s'], form_bond=['P5d'], break_bond=['P5d'], increment_radical=['P5d'], decrement_radical=['P5d'], increment_lone_pair=['P3d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5dd'].set_actions(increment_bond=['P5td'], decrement_bond=['P5d', 'P5dc'], form_bond=['P5dd'], break_bond=['P5dd'], increment_radical=['P5dd'], decrement_radical=['P5dd'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5dc'].set_actions(increment_bond=['P5dd', 'P5ddc', 'P5tc'], decrement_bond=['P5sc'], form_bond=['P5dc'], break_bond=['P5dc'], increment_radical=['P5dc'], decrement_radical=['P5dc'], increment_lone_pair=['P3d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5ddc'].set_actions(increment_bond=[], decrement_bond=['P5dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5t'].set_actions(increment_bond=['P5td'], decrement_bond=['P5d'], form_bond=['P5t'], break_bond=['P5t'], increment_radical=['P5t'], decrement_radical=['P5t'], increment_lone_pair=['P3t'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5td'].set_actions(increment_bond=[], decrement_bond=['P5t', 'P5dd'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5tc'].set_actions(increment_bond=[], decrement_bond=['P5dc'], form_bond=['P5tc'], break_bond=['P5tc'], increment_radical=['P5tc'], decrement_radical=['P5tc'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5b'].set_actions(increment_bond=['P5bd'], decrement_bond=[], form_bond=['P5b'], break_bond=['P5b'], increment_radical=['P5b'], decrement_radical=['P5b'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5bd'].set_actions(increment_bond=[], decrement_bond=['P5b'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['S'].set_actions(increment_bond=['S'], decrement_bond=['S'], form_bond=['S'], break_bond=['S'], increment_radical=['S'], decrement_radical=['S'], increment_lone_pair=['S'], decrement_lone_pair=['S'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S0sc'].set_actions(increment_bond=['S0sc'], decrement_bond=['S0sc'], form_bond=['S0sc'], break_bond=['Sa', 'S0sc'], increment_radical=['S0sc'], decrement_radical=['S0sc'], increment_lone_pair=[], decrement_lone_pair=['S2s', 'S2sc', 'S2dc', 'S2tc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sa'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['S0sc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['S2s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2s'].set_actions(increment_bond=['S2d', 'S2dc'], decrement_bond=[], form_bond=['S2s', 'S2sc'], break_bond=['S2s'], increment_radical=['S2s'], decrement_radical=['S2s'], increment_lone_pair=['Sa', 'S0sc'], decrement_lone_pair=['S4s', 'S4sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2sc'].set_actions(increment_bond=['S2dc'], decrement_bond=[], form_bond=['S2sc'], break_bond=['S2sc', 'S2s'], increment_radical=['S2sc'], decrement_radical=['S2sc'], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4s', 'S4sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2d'].set_actions(increment_bond=['S2tc'], decrement_bond=['S2s'], form_bond=['S2d'], break_bond=['S2d'], increment_radical=['S2d'], decrement_radical=['S2d'], increment_lone_pair=[], decrement_lone_pair=['S4dc', 'S4d'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2dc'].set_actions(increment_bond=['S2tc', 'S2dc'], decrement_bond=['S2sc', 'S2s', 'S2dc'], form_bond=['S2dc'], break_bond=['S2dc'], increment_radical=['S2dc'], decrement_radical=['S2dc'], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4d', 'S4dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2tc'].set_actions(increment_bond=[], decrement_bond=['S2d', 'S2dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4t'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4s'].set_actions(increment_bond=['S4d', 'S4dc'], decrement_bond=[], form_bond=['S4s'], break_bond=['S4s'], increment_radical=['S4s'], decrement_radical=['S4s'], increment_lone_pair=['S2s', 'S2sc'], decrement_lone_pair=['S6s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4sc'].set_actions(increment_bond=['S4d', 'S4dc'], decrement_bond=[], form_bond=['S4s', 'S4sc'], break_bond=['S4sc'], increment_radical=['S4sc'], decrement_radical=['S4sc'], increment_lone_pair=['S2s', 'S2sc'], decrement_lone_pair=['S6s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4d'].set_actions(increment_bond=['S4dd', 'S4dc', 'S4t', 'S4tdc'], decrement_bond=['S4s', 'S4sc'], form_bond=['S4dc', 'S4d'], break_bond=['S4d', 'S4dc'], increment_radical=['S4d'], decrement_radical=['S4d'], increment_lone_pair=['S2d', 'S2dc'], decrement_lone_pair=['S6d', 'S6dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4dc'].set_actions(increment_bond=['S4dd', 'S4dc', 'S4tdc'], decrement_bond=['S4sc', 'S4dc'], form_bond=['S4d', 'S4dc'], break_bond=['S4d', 'S4dc'], increment_radical=['S4dc'], decrement_radical=['S4dc'], increment_lone_pair=['S2d', 'S2dc'], decrement_lone_pair=['S6d', 'S6dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4dd'].set_actions(increment_bond=['S4dc'], decrement_bond=['S4dc', 'S4d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['S6dd'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4t'].set_actions(increment_bond=[], decrement_bond=['S4d'], form_bond=['S4t'], break_bond=['S4t'], increment_radical=['S4t'], decrement_radical=['S4t'], increment_lone_pair=['S2tc'], decrement_lone_pair=['S6t', 'S6tdc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4tdc'].set_actions(increment_bond=['S4tdc'], decrement_bond=['S4d', 'S4tdc'], form_bond=['S4tdc'], break_bond=['S4tdc'], increment_radical=['S4tdc'], decrement_radical=['S4tdc'], increment_lone_pair=['S6tdc'], decrement_lone_pair=['S6td', 'S6tdc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6s'].set_actions(increment_bond=['S6d', 'S6dc'], decrement_bond=[], form_bond=['S6s'], break_bond=['S6s'], increment_radical=['S6s'], decrement_radical=['S6s'], increment_lone_pair=['S4s', 'S4sc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6sc'].set_actions(increment_bond=['S6dc'], decrement_bond=[], form_bond=['S6sc'], break_bond=['S6sc'], increment_radical=['S6sc'], decrement_radical=['S6sc'], increment_lone_pair=['S4s', 'S4sc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6d'].set_actions(increment_bond=['S6dd', 'S6t', 'S6tdc'], decrement_bond=['S6s'], form_bond=['S6d', 'S6dc'], break_bond=['S6d', 'S6dc'], increment_radical=['S6d'], decrement_radical=['S6d'], increment_lone_pair=['S4d', 'S4dc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6dc'].set_actions(increment_bond=['S6dd', 'S6ddd', 'S6dc', 'S6t', 'S6td', 'S6tdc'], decrement_bond=['S6sc', 'S6dc'], form_bond=['S6d', 'S6dc'], break_bond=['S6d', 'S6dc'], increment_radical=['S6dc'], decrement_radical=['S6dc'], increment_lone_pair=['S4d', 'S4dc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6dd'].set_actions(increment_bond=['S6ddd', 'S6td'], decrement_bond=['S6d', 'S6dc'], form_bond=['S6dd', 'S6dc'], break_bond=['S6dd'], increment_radical=['S6dd'], decrement_radical=['S6dd'], increment_lone_pair=['S4dd'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6ddd'].set_actions(increment_bond=[], decrement_bond=['S6dd', 'S6dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6t'].set_actions(increment_bond=['S6td'], decrement_bond=['S6d', 'S6dc'], form_bond=['S6t'], break_bond=['S6t'], increment_radical=['S6t'], decrement_radical=['S6t'], increment_lone_pair=['S4t'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6td'].set_actions(increment_bond=['S6tt', 'S6tdc'], decrement_bond=['S6dc', 'S6t', 'S6dd', 'S6tdc'], form_bond=['S6td'], break_bond=['S6td'], increment_radical=['S6td'], decrement_radical=['S6td'], increment_lone_pair=['S4tdc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6tt'].set_actions(increment_bond=[], decrement_bond=['S6td', 'S6tdc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6tdc'].set_actions(increment_bond=['S6td', 'S6tdc', 'S6tt'], decrement_bond=['S6dc', 'S6tdc'], form_bond=['S6tdc'], break_bond=['S6tdc'], increment_radical=['S6tdc'], decrement_radical=['S6tdc'], increment_lone_pair=['S4t', 'S4tdc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['Cl'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Cl'], break_bond=['Cl'], increment_radical=['Cl'], decrement_radical=['Cl'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cl1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Cl1s'], break_bond=['Cl1s'], increment_radical=['Cl1s'], decrement_radical=['Cl1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['Br'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Br'], break_bond=['Br'], increment_radical=['Br'], decrement_radical=['Br'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Br1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Br1s'], break_bond=['Br1s'], increment_radical=['Br1s'], decrement_radical=['Br1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['I'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['I'], break_bond=['I'], increment_radical=['I'], decrement_radical=['I'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['I1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['I1s'], break_bond=['I1s'], increment_radical=['I1s'], decrement_radical=['I1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['F'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F'], break_bond=['F'], increment_radical=['F'], decrement_radical=['F'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['F1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F1s'], break_bond=['F1s'], increment_radical=['F1s'], decrement_radical=['F1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) # these are ordered in priority of picking if a more general atomtype is encountered -allElements = ['H', 'C', 'O', 'N', 'S', 'P', 'Si', 'F', 'Cl', 'Br', 'I', 'Ne', 'Ar', 'He', 'X'] +allElements = ['H', 'C', 'O', 'N', 'S', 'P', 'Si', 'F', 'Cl', 'Br', 'I', 'Ne', 'Ar', 'He', 'X', 'e'] # list of elements that do not have more specific atomTypes -nonSpecifics = ['H', 'He', 'Ne', 'Ar',] +nonSpecifics = ['He', 'Ne', 'Ar', 'e'] for atomtype in ATOMTYPES.values(): for items in [atomtype.generic, atomtype.specific, atomtype.increment_bond, atomtype.decrement_bond, atomtype.form_bond, atomtype.break_bond, atomtype.increment_radical, atomtype.decrement_radical, atomtype.increment_lone_pair, - atomtype.decrement_lone_pair]: + atomtype.decrement_lone_pair, atomtype.increment_charge, atomtype.decrement_charge]: for index in range(len(items)): items[index] = ATOMTYPES[items[index]] - def get_features(atom, bonds): """ Returns a list of features needed to determine atomtype for :class:'Atom' diff --git a/rmgpy/molecule/draw.py b/rmgpy/molecule/draw.py index d38c519197..37a3b47e25 100644 --- a/rmgpy/molecule/draw.py +++ b/rmgpy/molecule/draw.py @@ -109,9 +109,9 @@ class MoleculeDrawer(object): This class provides functionality for drawing the skeletal formula of molecules using the Cairo 2D graphics engine. The most common use case is simply:: - + MoleculeDrawer().draw(molecule, file_format='png', path='molecule.png') - + where ``molecule`` is the :class:`Molecule` object to draw. You can also pass a dict of options to the constructor to affect how the molecules are drawn. @@ -343,7 +343,7 @@ def _find_ring_groups(self): def _generate_coordinates(self, fix_surface_sites=True): """ - Generate the 2D coordinates to be used when drawing the current + Generate the 2D coordinates to be used when drawing the current molecule. The function uses rdKits 2D coordinate generation. Updates the self.coordinates Array in place. If `fix_surface_sites` is True, then the surface sites are placed @@ -406,7 +406,7 @@ def _generate_coordinates(self, fix_surface_sites=True): [-math.sin(angle), math.cos(angle)]], float) # need to keep self.coordinates and coordinates referring to the same object self.coordinates = coordinates = np.dot(coordinates, rot) - + # If two atoms lie on top of each other, push them apart a bit # This is ugly, but at least the mess you end up with isn't as misleading # as leaving everything piled on top of each other at the origin @@ -726,7 +726,7 @@ def _generate_ring_system_coordinates(self, atoms): def _generate_straight_chain_coordinates(self, atoms): """ Update the coordinates for the linear straight chain of `atoms` in - the current molecule. + the current molecule. """ coordinates = self.coordinates @@ -1374,6 +1374,8 @@ def _render_atom(self, symbol, atom, x0, y0, cr, heavy_first=True, draw_lone_pai cr.set_source_rgba(0.5, 0.0, 0.5, 1.0) elif heavy_atom == 'X': cr.set_source_rgba(0.5, 0.25, 0.5, 1.0) + elif heavy_atom == 'e': + cr.set_source_rgba(1.0, 0.0, 1.0, 1.0) else: cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) @@ -1567,7 +1569,7 @@ def _render_atom(self, symbol, atom, x0, y0, cr, heavy_first=True, draw_lone_pai cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.show_text(text) - # Draw lone electron pairs + # Draw lone electron pairs # Draw them for nitrogen containing molecules only if draw_lone_pairs: for i in range(atom.lone_pairs): @@ -1706,9 +1708,9 @@ class ReactionDrawer(object): This class provides functionality for drawing chemical reactions using the skeletal formula of each reactant and product molecule via the Cairo 2D graphics engine. The most common use case is simply:: - + ReactionDrawer().draw(reaction, file_format='png', path='reaction.png') - + where ``reaction`` is the :class:`Reaction` object to draw. You can also pass a dict of options to the constructor to affect how the molecules are drawn. @@ -1728,7 +1730,7 @@ def draw(self, reaction, file_format, path=None): Draw the given `reaction` using the given image `file_format` - pdf, svg, ps, or png. If `path` is given, the drawing is saved to that location on disk. - + This function returns the Cairo surface and context used to create the drawing, as well as a bounding box for the molecule being drawn as the tuple (`left`, `top`, `width`, `height`). diff --git a/rmgpy/molecule/element.py b/rmgpy/molecule/element.py index ad713d5ce6..0f5abfe4f4 100644 --- a/rmgpy/molecule/element.py +++ b/rmgpy/molecule/element.py @@ -78,7 +78,7 @@ def __init__(self, number, symbol, name, mass, isotope=-1, chemkin_name=None): self.mass = mass self.isotope = isotope self.chemkin_name = chemkin_name or self.name - if symbol in {'X','L','R'}: + if symbol in {'X','L','R','e'}: self.cov_radius = 0 else: try: @@ -122,11 +122,11 @@ class PeriodicSystem(object): https://sciencenotes.org/list-of-electronegativity-values-of-the-elements/ isotopes of the same element may have slight different electronegativities, which is not reflected below """ - valences = {'H': 1, 'He': 0, 'C': 4, 'N': 3, 'O': 2, 'F': 1, 'Ne': 0, + valences = {'H+':0, 'e': 0, 'H': 1, 'He': 0, 'C': 4, 'N': 3, 'O': 2, 'F': 1, 'Ne': 0, 'Si': 4, 'P': 3, 'S': 2, 'Cl': 1, 'Br': 1, 'Ar': 0, 'I': 1, 'X': 4} - valence_electrons = {'H': 1, 'He': 2, 'C': 4, 'N': 5, 'O': 6, 'F': 7, 'Ne': 8, + valence_electrons = {'H+':0, 'e': 1, 'H': 1, 'He': 2, 'C': 4, 'N': 5, 'O': 6, 'F': 7, 'Ne': 8, 'Si': 4, 'P': 5, 'S': 6, 'Cl': 7, 'Br': 7, 'Ar': 8, 'I': 7, 'X': 4} - lone_pairs = {'H': 0, 'He': 1, 'C': 0, 'N': 1, 'O': 2, 'F': 3, 'Ne': 4, + lone_pairs = {'H+':0, 'e': 0, 'H': 0, 'He': 1, 'C': 0, 'N': 1, 'O': 2, 'F': 3, 'Ne': 4, 'Si': 0, 'P': 1, 'S': 2, 'Cl': 3, 'Br': 3, 'Ar': 4, 'I': 3, 'X': 0} electronegativity = {'H': 2.20, 'D': 2.20, 'T': 2.20, 'C': 2.55, 'C13': 2.55, 'N': 3.04, 'O': 3.44, 'O18': 3.44, 'F': 3.98, 'Si': 1.90, 'P': 2.19, 'S': 2.58, 'Cl': 3.16, 'Br': 2.96, 'I': 2.66, 'X': 0.0} @@ -173,6 +173,8 @@ def get_element(value, isotope=-1): # Recommended IUPAC nomenclature is used throughout (including 'aluminium' and # 'caesium') +# electron +e = Element(-1, 'e', 'electron' , 5.486e-7) # Surface site X = Element(0, 'X', 'surface_site' , 0.0) @@ -310,6 +312,7 @@ def get_element(value, isotope=-1): # A list of the elements, sorted by increasing atomic number element_list = [ + e, X, H, D, T, He, Li, Be, B, C, C13, N, O, O18, F, Ne, diff --git a/rmgpy/molecule/group.py b/rmgpy/molecule/group.py index f4b0649c0d..5c4db3b8b3 100644 --- a/rmgpy/molecule/group.py +++ b/rmgpy/molecule/group.py @@ -80,7 +80,7 @@ class GroupAtom(Vertex): order to match. """ - def __init__(self, atomtype=None, radical_electrons=None, charge=None, label='', lone_pairs=None, site=None, morphology=None, + def __init__(self, atomtype=None, radical_electrons=None, charge=None, label='', lone_pairs=None, site=None, morphology=None, props=None): Vertex.__init__(self) self.atomtype = atomtype or [] @@ -115,7 +115,7 @@ def __reduce__(self): atomtype = self.atomtype if atomtype is not None: atomtype = [a.label for a in atomtype] - return (GroupAtom, (atomtype, self.radical_electrons, self.charge, self.label, self.lone_pairs, self.site, + return (GroupAtom, (atomtype, self.radical_electrons, self.charge, self.label, self.lone_pairs, self.site, self.morphology, self.props), d) def __setstate__(self, d): @@ -357,7 +357,7 @@ def equivalent(self, other, strict=True): where `other` can be either an :class:`Atom` or an :class:`GroupAtom` object. When comparing two :class:`GroupAtom` objects, this function respects wildcards, e.g. ``R!H`` is equivalent to ``C``. - + """ cython.declare(group=GroupAtom) if not strict: @@ -453,7 +453,7 @@ def is_specific_case_of(self, other): """ Returns ``True`` if `self` is the same as `other` or is a more specific case of `other`. Returns ``False`` if some of `self` is not - included in `other` or they are mutually exclusive. + included in `other` or they are mutually exclusive. """ cython.declare(group=GroupAtom) if not isinstance(other, GroupAtom): @@ -686,6 +686,7 @@ def make_sample_atom(self): 'I': 3, 'Ar': 4, 'X': 0, + 'e': 0 } for element_label in allElements: @@ -863,7 +864,7 @@ def is_single(self, wildcards=False): not. If `wildcards` is ``False`` we return False anytime there is more than one bond order, otherwise we return ``True`` if any of the options are single. - + NOTE: we can replace the absolute value relation with math.isclose when we swtich to python 3.5+ """ @@ -988,7 +989,7 @@ def is_hydrogen_bond(self, wildcards=False): return False else: return abs(self.order[0] - 0.1) <= 1e-9 and len(self.order) == 1 - + def is_reaction_bond(self, wildcards=False): """ Return ``True`` if the bond represents a reaction bond or ``False`` if @@ -1114,13 +1115,13 @@ class Group(Graph): """ A representation of a molecular substructure group using a graph data type, extending the :class:`Graph` class. The attributes are: - + =================== =================== ==================================== Attribute Type Description =================== =================== ==================================== `atoms` ``list`` Aliases for the `vertices` storing :class:`GroupAtom` `multiplicity` ``list`` Range of multiplicities accepted for the group - `props` ``dict`` Dictionary of arbitrary properties/flags classifying state of Group object + `props` ``dict`` Dictionary of arbitrary properties/flags classifying state of Group object `metal` ``list`` List of metals accepted for the group `facet` ``list`` List of facets accepted for the group =================== =================== ==================================== @@ -1565,10 +1566,10 @@ def specify_atom_extensions(self, i, basename, r): old_atom_type = grp.atoms[i].atomtype grp.atoms[i].atomtype = [item] grpc.atoms[i].atomtype = list(Rset - {item}) - + if len(grpc.atoms[i].atomtype) == 0: grpc = None - + if len(old_atom_type) > 1: labelList = [] old_atom_type_str = '' @@ -1632,10 +1633,10 @@ def specify_unpaired_extensions(self, i, basename, r_un): grpc = deepcopy(self) grp.atoms[i].radical_electrons = [item] grpc.atoms[i].radical_electrons = list(Rset - {item}) - + if len(grpc.atoms[i].radical_electrons) == 0: grpc = None - + atom_type = grp.atoms[i].atomtype if len(atom_type) > 1: @@ -1742,10 +1743,10 @@ def specify_bond_extensions(self, i, j, basename, r_bonds): grp.atoms[j].bonds[grp.atoms[i]].order = [bd] grpc.atoms[i].bonds[grpc.atoms[j]].order = list(Rbset - {bd}) grpc.atoms[j].bonds[grpc.atoms[i]].order = list(Rbset - {bd}) - + if len(list(Rbset - {bd})) == 0: grpc = None - + atom_type_i = grp.atoms[i].atomtype atom_type_j = grp.atoms[j].atomtype @@ -2067,7 +2068,7 @@ def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False): else: if group.facet: return [] - + # Do the isomorphism comparison return Graph.find_subgraph_isomorphisms(self, other, initial_map, save_order=save_order) @@ -2083,7 +2084,7 @@ def is_identical(self, other, save_order=False): if not isinstance(other, Group): raise TypeError( 'Got a {0} object for parameter "other", when a Group object is required.'.format(other.__class__)) - # An identical group is always a child of itself and + # An identical group is always a child of itself and # is the only case where that is true. Therefore # if we do both directions of isSubgraphIsmorphic, we need # to get True twice for it to be identical @@ -2899,8 +2900,20 @@ def make_sample_molecule(self): group_atom = mol_to_group[atom] else: raise UnexpectedChargeError(graph=new_molecule) - if atom.charge in group_atom.atomtype[0].charge: - # declared charge in atomtype is same as new charge + # check hardcoded atomtypes + positive_charged = ['H+', + 'Csc', 'Cdc', + 'N3sc', 'N5sc', 'N5dc', 'N5ddc', 'N5tc', 'N5b', + 'O2sc', 'O4sc', 'O4dc', 'O4tc', + 'P5sc', 'P5dc', 'P5ddc', 'P5tc', 'P5b', + 'S2sc', 'S4sc', 'S4dc', 'S4tdc', 'S6sc', 'S6dc', 'S6tdc'] + negative_charged = ['e', + 'C2sc', 'C2dc', 'C2tc', + 'N0sc', 'N1sc', 'N1dc', 'N5dddc', + 'O0sc', + 'P0sc', 'P1sc', 'P1dc', 'P5sc', + 'S0sc', 'S2sc', 'S2dc', 'S2tc', 'S4sc', 'S4dc', 'S4tdc', 'S6sc', 'S6dc', 'S6tdc'] + if group_atom.atomtype[0] in [ATOMTYPES[x] for x in positive_charged] and atom.charge > 0: pass elif atom.charge in group_atom.charge: # declared charge in original group is same as new charge diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index 03727be792..2433571081 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -2007,7 +2007,7 @@ def find_h_bonds(self): ONinds = [n for n, a in enumerate(self.atoms) if a.is_oxygen() or a.is_nitrogen()] for i, atm1 in enumerate(self.atoms): - if atm1.atomtype.label == 'H': + if atm1.atomtype.label == 'H0': atm_covs = [q for q in atm1.bonds.keys()] if len(atm_covs) > 1: # H is already H bonded continue diff --git a/rmgpy/molecule/translator.py b/rmgpy/molecule/translator.py index 731d8d8d8e..a19746b76f 100644 --- a/rmgpy/molecule/translator.py +++ b/rmgpy/molecule/translator.py @@ -103,7 +103,17 @@ """ multiplicity 1 1 X u0 + """, + 'e': + """ + multiplicity 1 + 1 e u0 p0 c-1 + """, + '[H+]': """ + multiplicity 1 + 1 H u0 p0 c+1 + """, } #: This dictionary is used to shortcut lookups of a molecule's SMILES string from its chemical formula. @@ -128,6 +138,8 @@ 'ClH': 'Cl', 'I2': '[I][I]', 'HI': 'I', + 'H': 'H+', + 'e': 'e' } RADICAL_LOOKUPS = { @@ -155,7 +167,8 @@ 'I': '[I]', 'CF': '[C]F', 'CCl': '[C]Cl', - 'CBr': '[C]Br' + 'CBr': '[C]Br', + 'e': 'e' } From 1f54116472bc8cdd74e2e06f52494fb1a6e6a634 Mon Sep 17 00:00:00 2001 From: ssun30 Date: Fri, 29 Mar 2024 12:31:56 -0400 Subject: [PATCH 005/109] Added increment_charge and decrement_charge to Rx and Rx!H atom types --- rmgpy/molecule/atomtype.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rmgpy/molecule/atomtype.py b/rmgpy/molecule/atomtype.py index 5bddd30a31..0097de9f4b 100644 --- a/rmgpy/molecule/atomtype.py +++ b/rmgpy/molecule/atomtype.py @@ -688,8 +688,8 @@ def get_features(self): single=[0,1], all_double=[0], r_double=[], o_double=[], s_double=[], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[3], charge=[0]) # examples for F1s: HF, [F], FO, CH3F, F2 -ATOMTYPES['Rx'].set_actions(increment_bond=['Rx'], decrement_bond=['Rx'], form_bond=['Rx'], break_bond=['Rx'], increment_radical=['Rx'], decrement_radical=['Rx'], increment_lone_pair=['Rx'], decrement_lone_pair=['Rx']) -ATOMTYPES['Rx!H'].set_actions(increment_bond=['Rx!H'], decrement_bond=['Rx!H'], form_bond=['Rx!H'], break_bond=['Rx!H'], increment_radical=['Rx!H'], decrement_radical=['Rx!H'], increment_lone_pair=['Rx!H'], decrement_lone_pair=['Rx!H']) +ATOMTYPES['Rx'].set_actions(increment_bond=['Rx'], decrement_bond=['Rx'], form_bond=['Rx'], break_bond=['Rx'], increment_radical=['Rx'], decrement_radical=['Rx'], increment_lone_pair=['Rx'], decrement_lone_pair=['Rx'], increment_charge=['Rx'], decrement_charge=['Rx']) +ATOMTYPES['Rx!H'].set_actions(increment_bond=['Rx!H'], decrement_bond=['Rx!H'], form_bond=['Rx!H'], break_bond=['Rx!H'], increment_radical=['Rx!H'], decrement_radical=['Rx!H'], increment_lone_pair=['Rx!H'], decrement_lone_pair=['Rx!H'], increment_charge=['Rx!H'], decrement_charge=['Rx!H']) ATOMTYPES['e'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) From b4f0e4be671f432065164d1cc934e39b69b5196d Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:06:46 -0400 Subject: [PATCH 006/109] added `gain_charge` and `lose_charge` actions --- rmgpy/molecule/group.py | 52 ++++++++++++++++++++++++++++++++++++++ rmgpy/molecule/molecule.py | 16 ++++++++++++ 2 files changed, 68 insertions(+) diff --git a/rmgpy/molecule/group.py b/rmgpy/molecule/group.py index 5c4db3b8b3..d41bb5d6d2 100644 --- a/rmgpy/molecule/group.py +++ b/rmgpy/molecule/group.py @@ -271,6 +271,54 @@ def _lose_radical(self, radical): # Set the new radical electron counts self.radical_electrons = radical_electrons + def _gain_charge(self, charge): + """ + Update the atom group as a result of applying a GAIN_CHARGE action, + where `charge` specifies the charge gained. + """ + atomtype = [] + + for atom in self.atomtype: + atomtype.extend(atom.increment_charge) + + if any([len(atom.increment_charge) == 0 for atom in self.atomtype]): + raise ActionError('Unable to update GroupAtom due to GAIN_CHARGE action: ' + 'Unknown atom type produced from set "{0}".'.format(self.atomtype)) + + if isinstance(self.charge,list): + charges = [] + for c in self.charge: + charges.append(c+charge) + self.charge = charges + else: + self.charge += 1 + + self.atomtype = list(set(atomtype)) + + def _lose_charge(self, charge): + """ + Update the atom group as a result of applying a LOSE_CHARGE action, + where `charge` specifies lost charge. + """ + atomtype = [] + + for atom in self.atomtype: + atomtype.extend(atom.decrement_charge) + + if any([len(atomtype.decrement_charge) == 0 for atomtype in self.atomtype]): + raise ActionError('Unable to update GroupAtom due to LOSE_CHARGE action: ' + 'Unknown atom type produced from set "{0}".'.format(self.atomtype)) + + if isinstance(self.charge,list): + charges = [] + for c in self.charge: + charges.append(c-charge) + self.charge = charges + else: + self.charge -= 1 + + self.atomtype = list(set(atomtype)) + def _gain_pair(self, pair): """ Update the atom group as a result of applying a GAIN_PAIR action, @@ -342,8 +390,12 @@ def apply_action(self, action): self._break_bond(action[2]) elif act == 'GAIN_RADICAL': self._gain_radical(action[2]) + elif act == 'GAIN_CHARGE': + self._gain_charge(action[2]) elif act == 'LOSE_RADICAL': self._lose_radical(action[2]) + elif act == 'LOSE_CHARGE': + self._lose_charge(action[2]) elif action[0].upper() == 'GAIN_PAIR': self._gain_pair(action[2]) elif action[0].upper() == 'LOSE_PAIR': diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index 2433571081..f7f599da22 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -503,6 +503,18 @@ def decrement_radical(self): raise gr.ActionError('Unable to update Atom due to LOSE_RADICAL action: ' 'Invalid radical electron set "{0}".'.format(self.radical_electrons)) + def increment_charge(self): + """ + Update the atom pattern as a result of applying a GAIN_CHARGE action + """ + self.charge += 1 + + def decrement_charge(self): + """ + Update the atom pattern as a result of applying a LOSE_CHARGE action + """ + self.charge -= 1 + def set_lone_pairs(self, lone_pairs): """ Set the number of lone electron pairs. @@ -566,6 +578,10 @@ def apply_action(self, action): for i in range(action[2]): self.increment_radical() elif act == 'LOSE_RADICAL': for i in range(abs(action[2])): self.decrement_radical() + elif act == 'GAIN_CHARGE': + for i in range(action[2]): self.increment_charge() + elif act == 'LOSE_CHARGE': + for i in range(abs(action[2])): self.decrement_charge() elif action[0].upper() == 'GAIN_PAIR': for i in range(action[2]): self.increment_lone_pairs() elif action[0].upper() == 'LOSE_PAIR': From f2dcafdbf9608fbaabc0523b443e07b6d92768d1 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:13:30 -0400 Subject: [PATCH 007/109] added `is_proton` and `is_electron` methods --- rmgpy/molecule/group.pxd | 8 ++++++++ rmgpy/molecule/group.py | 20 ++++++++++++++++++++ rmgpy/molecule/molecule.py | 25 +++++++++++++++++++++++++ rmgpy/species.pxd | 4 ++++ rmgpy/species.py | 16 ++++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/rmgpy/molecule/group.pxd b/rmgpy/molecule/group.pxd index afbd02f161..83b4ae83c6 100644 --- a/rmgpy/molecule/group.pxd +++ b/rmgpy/molecule/group.pxd @@ -74,6 +74,10 @@ cdef class GroupAtom(Vertex): cpdef bint is_bonded_to_surface(self) except -2 + cpdef bint is_proton(self) + + cpdef bint is_electron(self) + cpdef bint is_oxygen(self) cpdef bint is_sulfur(self) @@ -190,6 +194,10 @@ cdef class Group(Graph): cpdef bint is_surface_site(self) except -2 + cpdef bint is_proton(self) + + cpdef bint is_electron(self) + cpdef bint contains_surface_site(self) except -2 cpdef list get_surface_sites(self) diff --git a/rmgpy/molecule/group.py b/rmgpy/molecule/group.py index d41bb5d6d2..e12ac762ca 100644 --- a/rmgpy/molecule/group.py +++ b/rmgpy/molecule/group.py @@ -602,6 +602,18 @@ def is_bonded_to_surface(self): return True return False + def is_electron(self): + """ + Return ``True`` if the atom represents a surface site or ``False`` if not. + """ + return self.atomtype[0] == ATOMTYPES['e'] + + def is_proton(self): + """ + Return ``True`` if the atom represents a surface site or ``False`` if not. + """ + return self.atomtype[0] == ATOMTYPES['H+'] + def is_oxygen(self): """ Return ``True`` if the atom represents an oxygen atom or ``False`` if not. @@ -1308,6 +1320,14 @@ def get_surface_sites(self): cython.declare(atom=GroupAtom) return [atom for atom in self.atoms if atom.is_surface_site()] + def is_proton(self): + """Returns ``True`` iff the group is a proton""" + return len(self.atoms) == 1 and self.atoms[0].is_proton() + + def is_electron(self): + """Returns ``True`` iff the group is an electron""" + return len(self.atoms) == 1 and self.atoms[0].is_electron() + def remove_atom(self, atom): """ Remove `atom` and all bonds associated with it from the graph. Does diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index f7f599da22..dccf1637d3 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -354,6 +354,23 @@ def copy(self): a.props = deepcopy(self.props) return a + def is_electron(self): + """ + Return ``True`` if the atom represents an electron or ``False`` if + not. + """ + return self.element.number == -1 + + def is_proton(self): + """ + Return ``True`` if the atom represents a proton or ``False`` if + not. + """ + + if self.element.number == 1 and self.charge == 1: + return True + return False + def is_hydrogen(self): """ Return ``True`` if the atom represents a hydrogen atom or ``False`` if @@ -1191,6 +1208,14 @@ def is_surface_site(self): """Returns ``True`` iff the molecule is nothing but a surface site 'X'.""" return len(self.atoms) == 1 and self.atoms[0].is_surface_site() + def is_electron(self): + """Returns ``True`` iff the molecule is nothing but an electron 'e'.""" + return len(self.atoms) == 1 and self.atoms[0].is_electron() + + def is_proton(self): + """Returns ``True`` iff the molecule is nothing but a proton 'H+'.""" + return len(self.atoms) == 1 and self.atoms[0].is_proton() + def remove_atom(self, atom): """ Remove `atom` and all bonds associated with it from the graph. Does diff --git a/rmgpy/species.pxd b/rmgpy/species.pxd index 9922c1e59a..cf451edc2f 100644 --- a/rmgpy/species.pxd +++ b/rmgpy/species.pxd @@ -78,6 +78,10 @@ cdef class Species: cpdef bint is_surface_site(self) except -2 + cpdef bint is_electron(self) except -2 + + cpdef bint is_proton(self) except -2 + cpdef bint has_statmech(self) except -2 cpdef bint has_thermo(self) except -2 diff --git a/rmgpy/species.py b/rmgpy/species.py index e0ae40a2f0..bf3e8823de 100644 --- a/rmgpy/species.py +++ b/rmgpy/species.py @@ -495,6 +495,22 @@ def number_of_surface_sites(self): """ return self.molecule[0].number_of_surface_sites() + def is_electron(self): + """Return ``True`` if the species is an electron""" + + if len(self.molecule) == 0: + return False + else: + return self.molecule[0].is_electron() + + def is_proton(self): + """Return ``True`` if the species is a proton""" + + if len(self.molecule) == 0: + return False + else: + return self.molecule[0].is_proton() + def get_partition_function(self, T): """ Return the partition function for the species at the specified From 5160292b522ac96741ad0be5ad447b6a5a03c8ba Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:28:16 -0400 Subject: [PATCH 008/109] added updated charge method and revised `update` Molecule method the update method now updates lone pairs before updating charge --- rmgpy/molecule/molecule.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index dccf1637d3..1576ec81fd 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -573,6 +573,10 @@ def update_charge(self): if self.is_surface_site(): self.charge = 0 return + if self.is_electron(): + self.charge = -1 + return + valence_electron = elements.PeriodicSystem.valence_electrons[self.symbol] order = self.get_total_bond_order() self.charge = valence_electron - order - self.radical_electrons - 2 * self.lone_pairs @@ -1262,17 +1266,21 @@ def sort_atoms(self): for index, vertex in enumerate(self.vertices): vertex.sorting_label = index + def update_charge(self): + + for atom in self.atoms: + atom.update_charge() + def update(self, log_species=True, raise_atomtype_exception=True, sort_atoms=True): """ - Update the charge and atom types of atoms. + Update the lone_pairs, charge, and atom types of atoms. Update multiplicity, and sort atoms (if ``sort_atoms`` is ``True``) Does not necessarily update the connectivity values (which are used in isomorphism checks) If you need that, call update_connectivity_values() """ - for atom in self.atoms: - atom.update_charge() - + self.update_lone_pairs() + self.update_charge() self.update_atomtypes(log_species=log_species, raise_exception=raise_atomtype_exception) self.update_multiplicity() if sort_atoms: @@ -2344,7 +2352,7 @@ def update_lone_pairs(self): """ cython.declare(atom1=Atom, atom2=Atom, bond12=Bond, order=float) for atom1 in self.vertices: - if atom1.is_hydrogen() or atom1.is_surface_site(): + if atom1.is_hydrogen() or atom1.is_surface_site() or atom1.is_electron(): atom1.lone_pairs = 0 else: order = atom1.get_total_bond_order() From a2537c436b918233cdf8b76bec45fc40f4867dee Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:38:13 -0400 Subject: [PATCH 009/109] added `get_net_charge` method for Species --- rmgpy/species.pxd | 4 +++- rmgpy/species.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/rmgpy/species.pxd b/rmgpy/species.pxd index cf451edc2f..0e30ce2b0e 100644 --- a/rmgpy/species.pxd +++ b/rmgpy/species.pxd @@ -61,7 +61,9 @@ cdef class Species: cdef str _smiles cpdef generate_resonance_structures(self, bint keep_isomorphic=?, bint filter_structures=?, bint save_order=?) - + + cpdef get_net_charge(self) + cpdef bint is_isomorphic(self, other, bint generate_initial_map=?, bint save_order=?, bint strict=?) except -2 cpdef bint is_identical(self, other, bint strict=?) except -2 diff --git a/rmgpy/species.py b/rmgpy/species.py index bf3e8823de..0496940244 100644 --- a/rmgpy/species.py +++ b/rmgpy/species.py @@ -278,6 +278,14 @@ def molecular_weight(self): def molecular_weight(self, value): self._molecular_weight = quantity.Mass(value) + def get_net_charge(self): + """ + Iterate through the atoms in the structure and calculate the net charge + on the overall molecule. + """ + + return self.molecule[0].get_net_charge() + def generate_resonance_structures(self, keep_isomorphic=True, filter_structures=True, save_order=False): """ Generate all of the resonance structures of this species. The isomers are From 8241d4286c5a9372f0618b54e4679f8ed51d3cdf Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:28:56 -0400 Subject: [PATCH 010/109] do not raise charge exception by default when creating molecule and species from adj list --- rmgpy/molecule/molecule.py | 2 +- rmgpy/species.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index 1576ec81fd..287235db04 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -1859,7 +1859,7 @@ def from_smarts(self, smartsstr, raise_atomtype_exception=True): return self def from_adjacency_list(self, adjlist, saturate_h=False, raise_atomtype_exception=True, - raise_charge_exception=True, check_consistency=True): + raise_charge_exception=False, check_consistency=True): """ Convert a string adjacency list `adjlist` to a molecular structure. Skips the first line (assuming it's a label) unless `withLabel` is diff --git a/rmgpy/species.py b/rmgpy/species.py index 0496940244..2969dac82b 100644 --- a/rmgpy/species.py +++ b/rmgpy/species.py @@ -366,7 +366,7 @@ def is_structure_in_list(self, species_list): ' should be a List of Species objects.'.format(species)) return False - def from_adjacency_list(self, adjlist, raise_atomtype_exception=True, raise_charge_exception=True): + def from_adjacency_list(self, adjlist, raise_atomtype_exception=True, raise_charge_exception=False): """ Load the structure of a species as a :class:`Molecule` object from the given adjacency list `adjlist` and store it as the first entry of a From 82fb9b11b0a8d79a831d81e1ce9c1e4962c22bc1 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:30:12 -0400 Subject: [PATCH 011/109] try to generate_resonance_structures, and return a deep copy of the molecule if it fails --- rmgpy/molecule/molecule.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index 287235db04..0c10b51edf 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -2319,10 +2319,15 @@ def is_aryl_radical(self, aromatic_rings=None, save_order=False): def generate_resonance_structures(self, keep_isomorphic=False, filter_structures=True, save_order=False): """Returns a list of resonance structures of the molecule.""" - return resonance.generate_resonance_structures(self, keep_isomorphic=keep_isomorphic, + + try: + return resonance.generate_resonance_structures(self, keep_isomorphic=keep_isomorphic, filter_structures=filter_structures, save_order=save_order, ) + except: + logging.warning("Resonance structure generation failed for {}".format(self)) + return [self.copy(deep=True)] def get_url(self): """ From 4dbb61a0de5afb8f0e05055bd21ffd06bf0179f5 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:32:36 -0400 Subject: [PATCH 012/109] added molecule.pxd declarations --- rmgpy/molecule/molecule.pxd | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/rmgpy/molecule/molecule.pxd b/rmgpy/molecule/molecule.pxd index f227e5d533..335486f76f 100644 --- a/rmgpy/molecule/molecule.pxd +++ b/rmgpy/molecule/molecule.pxd @@ -56,6 +56,10 @@ cdef class Atom(Vertex): cpdef Vertex copy(self) + cpdef bint is_electron(self) + + cpdef bint is_proton(self) + cpdef bint is_hydrogen(self) cpdef bint is_non_hydrogen(self) @@ -89,7 +93,11 @@ cdef class Atom(Vertex): cpdef increment_radical(self) cpdef decrement_radical(self) - + + cpdef increment_charge(self) + + cpdef decrement_charge(self) + cpdef set_lone_pairs(self, int lone_pairs) cpdef increment_lone_pairs(self) @@ -166,6 +174,10 @@ cdef class Molecule(Graph): cpdef bint has_bond(self, Atom atom1, Atom atom2) + cpdef bint is_electron(self) + + cpdef bint is_proton(self) + cpdef bint contains_surface_site(self) cpdef bint is_surface_site(self) From 118271a101386a608f3de3c60befd1f93837561d Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:44:21 -0400 Subject: [PATCH 013/109] do not forbid ions however, we still need better thermo --- rmgpy/data/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rmgpy/data/base.py b/rmgpy/data/base.py index 54c0e6c070..a0a4d51444 100644 --- a/rmgpy/data/base.py +++ b/rmgpy/data/base.py @@ -1354,8 +1354,8 @@ def is_molecule_forbidden(self, molecule): raise NotImplementedError('Checking is only implemented for forbidden Groups, Molecule, and Species.') # Until we have more thermodynamic data of molecular ions we will forbid them - if molecule.get_net_charge() != 0: - return True + # if molecule.get_net_charge() != 0: + # return True return False From e271046f157b5ee51f9e6dc4d96e37dc2553ce5f Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:49:19 -0400 Subject: [PATCH 014/109] added `SurfaceChargeTransfer` and `SurfaceChargeTransferBEP` kinetic models --- rmgpy/data/kinetics/database.py | 4 +- rmgpy/data/kinetics/depository.py | 10 +- rmgpy/kinetics/__init__.py | 3 +- rmgpy/kinetics/surface.pxd | 60 +++- rmgpy/kinetics/surface.pyx | 520 ++++++++++++++++++++++++++++++ rmgpy/yml.py | 10 +- 6 files changed, 599 insertions(+), 8 deletions(-) diff --git a/rmgpy/data/kinetics/database.py b/rmgpy/data/kinetics/database.py index 91b13e3972..56c73ebf60 100644 --- a/rmgpy/data/kinetics/database.py +++ b/rmgpy/data/kinetics/database.py @@ -45,7 +45,8 @@ from rmgpy.kinetics import Arrhenius, ArrheniusEP, ThirdBody, Lindemann, Troe, \ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, \ Chebyshev, KineticsData, StickingCoefficient, \ - StickingCoefficientBEP, SurfaceArrhenius, SurfaceArrheniusBEP, ArrheniusBM + StickingCoefficientBEP, SurfaceArrhenius, SurfaceArrheniusBEP, \ + ArrheniusBM, SurfaceChargeTransfer from rmgpy.molecule import Molecule, Group from rmgpy.reaction import Reaction, same_species_lists from rmgpy.species import Species @@ -80,6 +81,7 @@ def __init__(self): 'StickingCoefficientBEP': StickingCoefficientBEP, 'SurfaceArrhenius': SurfaceArrhenius, 'SurfaceArrheniusBEP': SurfaceArrheniusBEP, + 'SurfaceChargeTransfer': SurfaceChargeTransfer, 'R': constants.R, 'ArrheniusBM': ArrheniusBM } diff --git a/rmgpy/data/kinetics/depository.py b/rmgpy/data/kinetics/depository.py index f3765860a9..188f476de1 100644 --- a/rmgpy/data/kinetics/depository.py +++ b/rmgpy/data/kinetics/depository.py @@ -35,6 +35,7 @@ from rmgpy.data.base import Database, Entry, DatabaseError from rmgpy.data.kinetics.common import save_entry +from rmgpy.kinetics import SurfaceChargeTransfer, SurfaceArrheniusBEP from rmgpy.reaction import Reaction @@ -60,7 +61,8 @@ def __init__(self, pairs=None, depository=None, family=None, - entry=None + entry=None, + electrons=None, ): Reaction.__init__(self, index=index, @@ -72,7 +74,8 @@ def __init__(self, transition_state=transition_state, duplicate=duplicate, degeneracy=degeneracy, - pairs=pairs + pairs=pairs, + electrons=electrons, ) self.depository = depository self.family = family @@ -187,6 +190,9 @@ def load(self, path, local_context=None, global_context=None): ''.format(product, self.label)) # Same comment about molecule vs species objects as above. rxn.products.append(species_dict[product]) + + if isinstance(entry.data, (SurfaceChargeTransfer, SurfaceArrheniusBEP)): + rxn.electrons = entry.data.electrons.value if not rxn.is_balanced(): raise DatabaseError('Reaction {0} in kinetics depository {1} was not balanced! Please reformulate.' diff --git a/rmgpy/kinetics/__init__.py b/rmgpy/kinetics/__init__.py index 0816d655f7..fe012a8642 100644 --- a/rmgpy/kinetics/__init__.py +++ b/rmgpy/kinetics/__init__.py @@ -35,4 +35,5 @@ from rmgpy.kinetics.kineticsdata import KineticsData, PDepKineticsData from rmgpy.kinetics.tunneling import Wigner, Eckart from rmgpy.kinetics.surface import SurfaceArrhenius, SurfaceArrheniusBEP, \ - StickingCoefficient, StickingCoefficientBEP + StickingCoefficient, StickingCoefficientBEP, \ + SurfaceChargeTransfer, SurfaceChargeTransferBEP diff --git a/rmgpy/kinetics/surface.pxd b/rmgpy/kinetics/surface.pxd index 8b43084d2a..0a834ea5a9 100644 --- a/rmgpy/kinetics/surface.pxd +++ b/rmgpy/kinetics/surface.pxd @@ -34,7 +34,7 @@ from rmgpy.quantity cimport ScalarQuantity, ArrayQuantity ################################################################################ cdef class StickingCoefficient(KineticsModel): - + cdef public ScalarQuantity _A cdef public ScalarQuantity _n cdef public ScalarQuantity _Ea @@ -50,7 +50,7 @@ cdef class StickingCoefficient(KineticsModel): cpdef bint is_similar_to(self, KineticsModel other_kinetics) except -2 cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 - + cpdef change_rate(self, double factor) cpdef to_html(self) @@ -71,10 +71,64 @@ cdef class StickingCoefficientBEP(KineticsModel): ################################################################################ cdef class SurfaceArrhenius(Arrhenius): + cdef public dict _coverage_dependence - pass + cpdef SurfaceChargeTransfer to_surface_charge_transfer(self, double V0, double electrons=?) + ################################################################################ cdef class SurfaceArrheniusBEP(ArrheniusEP): cdef public dict _coverage_dependence pass +################################################################################ +cdef class SurfaceChargeTransfer(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ScalarQuantity _Ea + cdef public ScalarQuantity _T0 + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _alpha + cdef public ScalarQuantity _electrons + + cpdef double get_activation_energy_from_potential(self, double V=?, bint non_negative=?) + + cpdef double get_rate_coefficient(self, double T, double V=?) except -1 + + cpdef change_rate(self, double factor) + + cpdef change_t0(self, double T0) + + cpdef change_v0(self, double V0) + + cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=?, np.ndarray weights=?, bint three_params=?) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef SurfaceArrhenius to_surface_arrhenius(self) + + cpdef SurfaceChargeTransferBEP to_surface_charge_transfer_bep(self, double dGrxn, double V0=?) + +################################################################################ +cdef class SurfaceChargeTransferBEP(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ScalarQuantity _E0 + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _alpha + cdef public ScalarQuantity _electrons + + cpdef change_v0(self, double V0) + + cpdef double get_activation_energy(self, double dGrxn) except -1 + + cpdef double get_activation_energy_from_potential(self, double V, double dGrxn) except -1 + + cpdef double get_rate_coefficient_from_potential(self, double T, double V, double dGrxn) except -1 + + cpdef SurfaceChargeTransfer to_surface_charge_transfer(self, double dGrxn) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef change_rate(self, double factor) diff --git a/rmgpy/kinetics/surface.pyx b/rmgpy/kinetics/surface.pyx index 120cfc006d..679f7bb338 100644 --- a/rmgpy/kinetics/surface.pyx +++ b/rmgpy/kinetics/surface.pyx @@ -570,6 +570,25 @@ cdef class SurfaceArrhenius(Arrhenius): return (SurfaceArrhenius, (self.A, self.n, self.Ea, self.T0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.coverage_dependence, self.uncertainty, self.comment)) + cpdef SurfaceChargeTransfer to_surface_charge_transfer(self, double V0, double electrons=-1): + """ + Return an :class:`SurfaceChargeTransfer` instance of the kinetics model with reversible + potential `V0` in Volts and electron stochiometric coeff ` electrons` + """ + return SurfaceChargeTransfer( + A=self.A, + n=self.n, + electrons= electrons, + Ea=self.Ea, + V0=(V0,'V'), + T0=(1, "K"), + Tmin=self.Tmin, + Tmax=self.Tmax, + uncertainty = self.uncertainty, + comment=self.comment, + ) + + ################################################################################ cdef class SurfaceArrheniusBEP(ArrheniusEP): @@ -684,3 +703,504 @@ cdef class SurfaceArrheniusBEP(ArrheniusEP): coverage_dependence=self.coverage_dependence, comment=self.comment, ) + +################################################################################ + +cdef class SurfaceChargeTransfer(KineticsModel): + + """ + A kinetics model for surface charge transfer reactions + + It is very similar to the :class:`SurfaceArrhenius`, but the Ea is potential-dependent + + + The attributes are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `T0` The reference temperature + `n` The temperature exponent + `Ea` The activation energy + `electrons` The stochiometry coeff for electrons (negative if reactant, positive if product) + `V0` The reference potential + `alpha` The charge transfer coefficient + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, Ea=None, V0=None, alpha=0.5, electrons=-1, T0=(1.0, "K"), Tmin=None, Tmax=None, + Pmin=None, Pmax=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, + comment=comment) + + self.alpha = alpha + self.A = A + self.n = n + self.Ea = Ea + self.T0 = T0 + self.electrons = electrons + self.V0 = V0 + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Arrhenius object. + """ + string = 'SurfaceChargeTransfer(A={0!r}, n={1!r}, Ea={2!r}, V0={3!r}, alpha={4!r}, electrons={5!r}, T0={6!r}'.format( + self.A, self.n, self.Ea, self.V0, self.alpha, self.electrons, self.T0) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a SurfaceChargeTransfer object. + """ + return (SurfaceChargeTransfer, (self.A, self.n, self.Ea, self.V0, self.alpha, self.electrons, self.T0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.SurfaceRateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property Ea: + """The activation energy.""" + def __get__(self): + return self._Ea + def __set__(self, value): + self._Ea = quantity.Energy(value) + + property T0: + """The reference temperature.""" + def __get__(self): + return self._T0 + def __set__(self, value): + self._T0 = quantity.Temperature(value) + + property V0: + """The reference potential.""" + def __get__(self): + return self._V0 + def __set__(self, value): + self._V0 = quantity.Potential(value) + + property electrons: + """The number of electrons transferred.""" + def __get__(self): + return self._electrons + def __set__(self, value): + self._electrons = quantity.Dimensionless(value) + + property alpha: + """The charge transfer coefficient.""" + def __get__(self): + return self._alpha + def __set__(self, value): + self._alpha = quantity.Dimensionless(value) + + cpdef double get_activation_energy_from_potential(self, double V=0.0, bint non_negative=True): + """ + Return the effective activation energy (in J/mol) at specificed potential (in Volts). + """ + cdef double electrons, alpha, Ea, V0 + + electrons = self._electrons.value_si + alpha = self._alpha.value_si + Ea = self._Ea.value_si + V0 = self._V0.value_si + + Ea -= alpha * electrons * constants.F * (V-V0) + + if non_negative is True: + if Ea < 0: + Ea = 0.0 + + return Ea + + cpdef double get_rate_coefficient(self, double T, double V=0.0) except -1: + """ + Return the rate coefficient in the appropriate combination of m^2, + mol, and s at temperature `T` in K. + """ + cdef double A, n, V0, T0, Ea + + A = self._A.value_si + n = self._n.value_si + V0 = self._V0.value_si + T0 = self._T0.value_si + + if V != V0: + Ea = self.get_activation_energy_from_potential(V) + else: + Ea = self._Ea.value_si + + return A * (T / T0) ** n * exp(-Ea / (constants.R * T)) + + cpdef change_t0(self, double T0): + """ + Changes the reference temperature used in the exponent to `T0` in K, + and adjusts the preexponential factor accordingly. + """ + self._A.value_si /= (self._T0.value_si / T0) ** self._n.value_si + self._T0.value_si = T0 + + cpdef change_v0(self, double V0): + """ + Changes the reference potential to `V0` in volts, and adjusts the + activation energy `Ea` accordingly. + """ + + self._Ea.value_si = self.get_activation_energy_from_potential(V0) + self._V0.value_si = V0 + + cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=1, + np.ndarray weights=None, bint three_params=False): + """ + Fit the Arrhenius parameters to a set of rate coefficient data `klist` + in units of `kunits` corresponding to a set of temperatures `Tlist` in + K. A linear least-squares fit is used, which guarantees that the + resulting parameters provide the best possible approximation to the + data. + """ + import scipy.stats + if not all(np.isfinite(klist)): + raise ValueError("Rates must all be finite, not inf or NaN") + if any(klist<0): + if not all(klist<0): + raise ValueError("Rates must all be positive or all be negative.") + rate_sign_multiplier = -1 + klist = -1 * klist + else: + rate_sign_multiplier = 1 + + assert len(Tlist) == len(klist), "length of temperatures and rates must be the same" + if len(Tlist) < 3 + three_params: + raise KineticsError('Not enough degrees of freedom to fit this Arrhenius expression') + if three_params: + A = np.zeros((len(Tlist), 3), np.float64) + A[:, 0] = np.ones_like(Tlist) + A[:, 1] = np.log(Tlist / T0) + A[:, 2] = -1.0 / constants.R / Tlist + else: + A = np.zeros((len(Tlist), 2), np.float64) + A[:, 0] = np.ones_like(Tlist) + A[:, 1] = -1.0 / constants.R / Tlist + b = np.log(klist) + if weights is not None: + for n in range(b.size): + A[n, :] *= weights[n] + b[n] *= weights[n] + x, residues, rank, s = np.linalg.lstsq(A, b, rcond=RCOND) + + # Determine covarianace matrix to obtain parameter uncertainties + count = klist.size + cov = residues[0] / (count - 3) * np.linalg.inv(np.dot(A.T, A)) + t = scipy.stats.t.ppf(0.975, count - 3) + + if not three_params: + x = np.array([x[0], 0, x[1]]) + cov = np.array([[cov[0, 0], 0, cov[0, 1]], [0, 0, 0], [cov[1, 0], 0, cov[1, 1]]]) + + self.A = (rate_sign_multiplier * exp(x[0]), kunits) + self.n = x[1] + self.Ea = (x[2] * 0.001, "kJ/mol") + self.T0 = (T0, "K") + self.Tmin = (np.min(Tlist), "K") + self.Tmax = (np.max(Tlist), "K") + self.comment = 'Fitted to {0:d} data points; dA = *|/ {1:g}, dn = +|- {2:g}, dEa = +|- {3:g} kJ/mol'.format( + len(Tlist), + exp(sqrt(cov[0, 0])), + sqrt(cov[1, 1]), + sqrt(cov[2, 2]) * 0.001, + ) + + return self + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, SurfaceChargeTransfer): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.Ea.equals(other_kinetics.Ea) or not self.T0.equals(other_kinetics.T0) + or not self.alpha.equals(other_kinetics.alpha) or not self.electrons.equals(other_kinetics.electrons) + or not self.V0.equals(other_kinetics.V0)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor in Arrhenius expression by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + + cpdef SurfaceArrhenius to_surface_arrhenius(self): + """ + Return an :class:`SurfaceArrhenius` instance of the kinetics model + """ + return SurfaceArrhenius( + A=self.A, + n=self.n, + Ea=self.Ea, + T0=(1, "K"), + Tmin=self.Tmin, + Tmax=self.Tmax, + uncertainty = self.uncertainty, + comment=self.comment, + ) + + cpdef SurfaceChargeTransferBEP to_surface_charge_transfer_bep(self, double dGrxn, double V0=0.0): + """ + Converts an SurfaceChargeTransfer object to SurfaceChargeTransferBEP + """ + cdef double E0 + + self.change_t0(1) + self.change_v0(V0) + + E0 = self.Ea.value_si - self._alpha.value_si * dGrxn + if E0 < 0: + E0 = 0.0 + + aep = SurfaceChargeTransferBEP( + A=self.A, + electrons=self.electrons, + n=self.n, + alpha=self.alpha, + V0=self.V0, + E0=(E0, 'J/mol'), + Tmin=self.Tmin, + Tmax=self.Tmax, + Pmin=self.Pmin, + Pmax=self.Pmax, + uncertainty=self.uncertainty, + comment=self.comment) + return aep + +cdef class SurfaceChargeTransferBEP(KineticsModel): + """ + A kinetics model based on the (modified) Arrhenius equation, using the + Evans-Polanyi equation to determine the activation energy. The attributes + are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `n` The temperature exponent + `E0` The activation energy at equilibiurm + ` electrons` The stochiometry coeff for electrons (negative if reactant, positive if product) + `V0` The reference potential + `alpha` The charge transfer coefficient + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, E0=None, V0=(0.0,'V'), alpha=0.5, electrons=-1, Tmin=None, Tmax=None, + Pmin=None, Pmax=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, + comment=comment) + + self.alpha = alpha + self.A = A + self.n = n + self.E0 = E0 + self.electrons = electrons + self.V0 = V0 + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Arrhenius object. + """ + string = 'SurfaceChargeTransferBEP(A={0!r}, n={1!r}, E0={2!r}, V0={3!r}, alpha={4!r}, electrons={5!r}'.format( + self.A, self.n, self.E0, self.V0, self.alpha, self.electrons) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a SurfaceChargeTransfer object. + """ + return (SurfaceChargeTransferBEP, (self.A, self.n, self.E0, self.V0, self.alpha, self.electrons, self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.SurfaceRateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property E0: + """The activation energy.""" + def __get__(self): + return self._E0 + def __set__(self, value): + self._E0 = quantity.Energy(value) + + property V0: + """The reference potential.""" + def __get__(self): + return self._V0 + def __set__(self, value): + self._V0 = quantity.Potential(value) + + property electrons: + """The number of electrons transferred.""" + def __get__(self): + return self._electrons + def __set__(self, value): + self._electrons = quantity.Dimensionless(value) + + property alpha: + """The charge transfer coefficient.""" + def __get__(self): + return self._alpha + def __set__(self, value): + self._alpha = quantity.Dimensionless(value) + + cpdef change_v0(self, double V0): + """ + Changes the reference potential to `V0` in volts, and adjusts the + activation energy `E0` accordingly. + """ + + self._E0.value_si = self.get_activation_energy_from_potential(V0,0.0) + self._V0.value_si = V0 + + cpdef double get_activation_energy(self, double dGrxn) except -1: + """ + Return the activation energy in J/mol corresponding to the given + free energy of reaction `dGrxn` in J/mol at the reference potential. + """ + cdef double Ea + Ea = self._alpha.value_si * dGrxn + self._E0.value_si + + if Ea < 0.0: + Ea = 0.0 + elif dGrxn > 0.0 and Ea < dGrxn: + Ea = dGrxn + + return Ea + + cpdef double get_activation_energy_from_potential(self, double V, double dGrxn) except -1: + """ + Return the activation energy in J/mol corresponding to the given + free energy of reaction `dGrxn` in J/mol. + """ + cdef double Ea + Ea = self.get_activation_energy(dGrxn) + Ea -= self._alpha.value_si * self._electrons.value_si * constants.F * (V-self._V0.value_si) + + return Ea + + cpdef double get_rate_coefficient_from_potential(self, double T, double V, double dGrxn) except -1: + """ + Return the rate coefficient in the appropriate combination of m^3, + mol, and s at temperature `T` in K, potential `V` in volts, and + free of reaction `dGrxn` in J/mol. + """ + cdef double A, n, Ea + Ea = self.get_activation_energy_from_potential(V,dGrxn) + A = self._A.value_si + n = self._n.value_si + return A * T ** n * exp(-Ea / (constants.R * T)) + + cpdef SurfaceChargeTransfer to_surface_charge_transfer(self, double dGrxn): + """ + Return an :class:`SurfaceChargeTransfer` instance of the kinetics model using the + given free energy of reaction `dGrxn` to determine the activation energy. + """ + return SurfaceChargeTransfer( + A=self.A, + n=self.n, + electrons=self.electrons, + Ea=(self.get_activation_energy(dGrxn) * 0.001, "kJ/mol"), + V0=self.V0, + T0=(1, "K"), + Tmin=self.Tmin, + Tmax=self.Tmax, + Pmin=self.Pmin, + Pmax=self.Pmax, + uncertainty=self.uncertainty, + comment=self.comment, + ) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, SurfaceChargeTransferBEP): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.E0.equals(other_kinetics.E0) or not self.alpha.equals(other_kinetics.alpha) + or not self.electrons.equals(other_kinetics.electrons) or not self.V0.equals(other_kinetics.V0)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + + def set_cantera_kinetics(self, ct_reaction, species_list): + """ + Sets a cantera ElementaryReaction() object with the modified Arrhenius object + converted to an Arrhenius form. + """ + raise NotImplementedError('set_cantera_kinetics() is not implemented for ArrheniusEP class kinetics.') diff --git a/rmgpy/yml.py b/rmgpy/yml.py index cd8b9de5b5..5d78934a80 100644 --- a/rmgpy/yml.py +++ b/rmgpy/yml.py @@ -44,7 +44,7 @@ from rmgpy.kinetics.falloff import Troe, ThirdBody, Lindemann from rmgpy.kinetics.chebyshev import Chebyshev from rmgpy.data.solvation import SolventData -from rmgpy.kinetics.surface import StickingCoefficient +from rmgpy.kinetics.surface import StickingCoefficient, SurfaceChargeTransfer from rmgpy.util import make_output_subdirectory @@ -148,6 +148,14 @@ def obj_to_dict(obj, spcs, names=None, label="solvent"): result_dict["A"] = obj.A.value_si result_dict["Ea"] = obj.Ea.value_si result_dict["n"] = obj.n.value_si + elif isinstance(obj, SurfaceChargeTransfer): + result_dict["type"] = "SurfaceChargeTransfer" + result_dict["A"] = obj.A.value_si + result_dict["Ea"] = obj.Ea.value_si + result_dict["n"] = obj.n.value_si + result_dict["electrons"] = obj.electrons.value_si + result_dict["V0"] = obj.V0.value_si + result_dict["alpha"] = obj.alpha.value_si elif isinstance(obj, StickingCoefficient): obj.change_t0(1.0) result_dict["type"] = "StickingCoefficient" From 67fe9f26655c69b6a3cb52eab95e66f9bb70f1be Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 16:35:12 -0400 Subject: [PATCH 015/109] making rules for SurfaceChargeTransfer training reactions --- rmgpy/data/kinetics/rules.py | 40 ++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/rmgpy/data/kinetics/rules.py b/rmgpy/data/kinetics/rules.py index 6f20e5460a..a87cfb8a14 100644 --- a/rmgpy/data/kinetics/rules.py +++ b/rmgpy/data/kinetics/rules.py @@ -44,7 +44,8 @@ from rmgpy.data.base import Database, Entry, get_all_combinations from rmgpy.data.kinetics.common import save_entry from rmgpy.exceptions import KineticsError, DatabaseError -from rmgpy.kinetics import ArrheniusEP, Arrhenius, StickingCoefficientBEP, SurfaceArrheniusBEP +from rmgpy.kinetics import ArrheniusEP, Arrhenius, StickingCoefficientBEP, SurfaceArrheniusBEP, \ + SurfaceChargeTransfer, SurfaceChargeTransferBEP from rmgpy.quantity import Quantity, ScalarQuantity from rmgpy.reaction import Reaction @@ -282,12 +283,23 @@ def _get_average_kinetics(self, kinetics_list): n = 0.0 E0 = 0.0 alpha = 0.0 - count = len(kinetics_list) + electrons = None + V0 = None + count = 0 for kinetics in kinetics_list: + if isinstance(kinetics, SurfaceChargeTransfer): + continue + count += 1 logA += math.log10(kinetics.A.value_si) n += kinetics.n.value_si alpha += kinetics.alpha.value_si E0 += kinetics.E0.value_si + if isinstance(kinetics, SurfaceChargeTransferBEP): + if electrons is None: + electrons = kinetics.electrons.value_si + if V0 is None: + V0 = kinetics.V0.value_si + logA /= count n /= count alpha /= count @@ -312,15 +324,25 @@ def _get_average_kinetics(self, kinetics_list): else: raise Exception('Invalid units {0} for averaging kinetics.'.format(Aunits)) - if type(kinetics) not in [ArrheniusEP, SurfaceArrheniusBEP, StickingCoefficientBEP]: + if type(kinetics) not in [ArrheniusEP, SurfaceArrheniusBEP, StickingCoefficientBEP, SurfaceChargeTransferBEP]: raise Exception('Invalid kinetics type {0!r} for {1!r}.'.format(type(kinetics), self)) - averaged_kinetics = type(kinetics)( - A=(10 ** logA, Aunits), - n=n, - alpha=alpha, - E0=(E0 * 0.001, "kJ/mol"), - ) + if isinstance(kinetics, SurfaceChargeTransferBEP): + averaged_kinetics = SurfaceChargeTransferBEP( + A=(10 ** logA, Aunits), + n=n, + electrons=electrons, + alpha=alpha, + V0=(V0,'V'), + E0=(E0 * 0.001, "kJ/mol"), + ) + else: + averaged_kinetics = type(kinetics)( + A=(10 ** logA, Aunits), + n=n, + alpha=alpha, + E0=(E0 * 0.001, "kJ/mol"), + ) return averaged_kinetics def estimate_kinetics(self, template, degeneracy=1): From 5ae918a8f9e113968bb661ecf558eec57ffcfed2 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 15:15:21 -0400 Subject: [PATCH 016/109] added `electrons` reaction attr and reaction methods for charge transfer reactions --- rmgpy/reaction.pxd | 31 +++- rmgpy/reaction.py | 363 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 306 insertions(+), 88 deletions(-) diff --git a/rmgpy/reaction.pxd b/rmgpy/reaction.pxd index 7755ba2311..1b073a1296 100644 --- a/rmgpy/reaction.pxd +++ b/rmgpy/reaction.pxd @@ -32,7 +32,7 @@ from rmgpy.molecule.graph cimport Vertex, Graph from rmgpy.molecule.element cimport Element from rmgpy.kinetics.model cimport KineticsModel from rmgpy.kinetics.arrhenius cimport Arrhenius -from rmgpy.kinetics.surface cimport SurfaceArrhenius, StickingCoefficient +from rmgpy.kinetics.surface cimport SurfaceArrhenius, StickingCoefficient, SurfaceChargeTransfer cimport numpy as np @@ -49,8 +49,11 @@ cdef class Reaction: cdef public KineticsModel kinetics cdef public Arrhenius network_kinetics cdef public SurfaceArrhenius + cdef public SurfaceChargeTransfer cdef public bint duplicate cdef public float _degeneracy + cdef public int electrons + cdef public int _protons cdef public list pairs cdef public bint allow_pdep_route cdef public bint elementary_high_p @@ -70,6 +73,10 @@ cdef class Reaction: cpdef bint is_surface_reaction(self) + cpdef bint is_charge_transfer_reaction(self) + + cpdef bint is_surface_charge_transfer_reaction(self) + cpdef bint has_template(self, list reactants, list products) cpdef bint matches_species(self, list reactants, list products=?) @@ -78,27 +85,35 @@ cdef class Reaction: bint check_only_label=?, bint check_template_rxn_products=?, bint generate_initial_map=?, bint strict=?, bint save_order=?) except -2 + cpdef double _apply_CHE_model(self, double T) + cpdef double get_enthalpy_of_reaction(self, double T) cpdef double get_entropy_of_reaction(self, double T) - cpdef double get_free_energy_of_reaction(self, double T) + cpdef double _get_free_energy_of_charge_transfer_reaction(self, double T, double potential=?) + + cpdef double get_free_energy_of_reaction(self, double T, double potential=?) - cpdef double get_equilibrium_constant(self, double T, str type=?, double surface_site_density=?) + cpdef double get_reversible_potential(self, double T) + + cpdef double set_reference_potential(self, double T) + + cpdef double get_equilibrium_constant(self, double T, double potential=?, str type=?, double surface_site_density=?) cpdef np.ndarray get_enthalpies_of_reaction(self, np.ndarray Tlist) cpdef np.ndarray get_entropies_of_reaction(self, np.ndarray Tlist) - cpdef np.ndarray get_free_energies_of_reaction(self, np.ndarray Tlist) + cpdef np.ndarray get_free_energies_of_reaction(self, np.ndarray Tlist, double potential=?) - cpdef np.ndarray get_equilibrium_constants(self, np.ndarray Tlist, str type=?) + cpdef np.ndarray get_equilibrium_constants(self, np.ndarray Tlist, double potential=?, str type=?) cpdef int get_stoichiometric_coefficient(self, Species spec) - cpdef double get_rate_coefficient(self, double T, double P=?, double surface_site_density=?) + cpdef double get_rate_coefficient(self, double T, double P=?, double surface_site_density=?, double potential=?) - cpdef double get_surface_rate_coefficient(self, double T, double surface_site_density) except -2 + cpdef double get_surface_rate_coefficient(self, double T, double surface_site_density, double potential=?) except -2 cpdef fix_barrier_height(self, bint force_positive=?) @@ -108,6 +123,8 @@ cdef class Reaction: cpdef reverse_sticking_coeff_rate(self, StickingCoefficient k_forward, str reverse_units, double surface_site_density, Tmin=?, Tmax=?) + cpdef reverse_surface_charge_transfer_rate(self, SurfaceChargeTransfer k_forward, str reverse_units, Tmin=?, Tmax=?) + cpdef generate_reverse_rate_coefficient(self, bint network_kinetics=?, Tmin=?, Tmax=?, double surface_site_density=?) cpdef np.ndarray calculate_tst_rate_coefficients(self, np.ndarray Tlist) diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index f10e1dcefa..f7d7b2cfea 100644 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -30,8 +30,8 @@ """ This module contains classes and functions for working with chemical reactions. -From the `IUPAC Compendium of Chemical Terminology -`_, a chemical reaction is "a process that +From the `IUPAC Compendium of Chemical Terminology +`_, a chemical reaction is "a process that results in the interconversion of chemical species". In RMG Py, a chemical reaction is represented in memory as a :class:`Reaction` @@ -55,12 +55,13 @@ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, get_rate_coefficient_units_from_reaction_order, \ SurfaceArrheniusBEP, StickingCoefficientBEP from rmgpy.kinetics.arrhenius import Arrhenius # Separate because we cimport from rmgpy.kinetics.arrhenius -from rmgpy.kinetics.surface import SurfaceArrhenius, StickingCoefficient # Separate because we cimport from rmgpy.kinetics.surface +from rmgpy.kinetics.surface import SurfaceArrhenius, StickingCoefficient, SurfaceChargeTransfer, SurfaceChargeTransferBEP # Separate because we cimport from rmgpy.kinetics.surface from rmgpy.kinetics.diffusionLimited import diffusion_limiter from rmgpy.molecule.element import Element, element_list from rmgpy.molecule.molecule import Molecule, Atom from rmgpy.pdep.reaction import calculate_microcanonical_rate_coefficient from rmgpy.species import Species +from rmgpy.thermo import ThermoData ################################################################################ @@ -68,7 +69,7 @@ class Reaction: """ A chemical reaction. The attributes are: - + =================== =========================== ============================ Attribute Type Description =================== =========================== ============================ @@ -92,7 +93,7 @@ class Reaction: `is_forward` ``bool`` Indicates if the reaction was generated in the forward (true) or reverse (false) `rank` ``int`` Integer indicating the accuracy of the kinetics for this reaction =================== =========================== ============================ - + """ def __init__(self, @@ -110,10 +111,11 @@ def __init__(self, pairs=None, allow_pdep_route=False, elementary_high_p=False, - allow_max_rate_violation=False, rank=None, + electrons=0, comment='', is_forward=None, + allow_max_rate_violation=False, ): self.index = index self.label = label @@ -129,11 +131,12 @@ def __init__(self, self.pairs = pairs self.allow_pdep_route = allow_pdep_route self.elementary_high_p = elementary_high_p + self.rank = rank + self.electrons = electrons self.comment = comment self.k_effective_cache = {} self.is_forward = is_forward self.allow_max_rate_violation = allow_max_rate_violation - self.rank = rank def __repr__(self): """ @@ -157,7 +160,8 @@ def __repr__(self): if self.elementary_high_p: string += 'elementary_high_p={0}, '.format(self.elementary_high_p) if self.comment != '': string += 'comment={0!r}, '.format(self.comment) if self.rank is not None: string += 'rank={0!r},'.format(self.rank) - string = string[:-2] + ')' + if self.electrons != 0: string += 'electrons={0:d},'.format(self.electrons) + string = string[:-1] + ')' return string def __str__(self): @@ -169,7 +173,7 @@ def __str__(self): def to_labeled_str(self, use_index=False): """ - the same as __str__ except that the labels are assumed to exist and used for reactant and products rather than + the same as __str__ except that the labels are assumed to exist and used for reactant and products rather than the labels plus the index in parentheses """ arrow = ' <=> ' if self.reversible else ' => ' @@ -198,7 +202,8 @@ def __reduce__(self): self.allow_pdep_route, self.elementary_high_p, self.rank, - self.comment + self.electrons, + self.comment, )) @property @@ -231,10 +236,28 @@ def degeneracy(self, new): # set new degeneracy self._degeneracy = new + @property + def protons(self): + """ + The stochiometric coeff for protons in charge transfer reactions + """ + if self.is_charge_transfer_reaction(): + self._protons = 0 + for prod in self.products: + if prod.is_proton(): + self._protons += 1 + for react in self.reactants: + if react.is_proton(): + self._protons -= 1 + else: + self._protons = 0 + + return self._protons + def to_chemkin(self, species_list=None, kinetics=True): """ Return the chemkin-formatted string for this reaction. - + If `kinetics` is set to True, the chemkin format kinetics will also be returned (requires the `species_list` to figure out third body colliders.) Otherwise, only the reaction string will be returned. @@ -363,9 +386,9 @@ def to_cantera(self, species_list=None, use_chemkin_identifier=False): if isinstance(ct_reaction, list): for rxn in ct_reaction: rxn.reversible = self.reversible - # Set the duplicate flag to true since this reaction comes from multiarrhenius or multipdeparrhenius + # Set the duplicate flag to true since this reaction comes from multiarrhenius or multipdeparrhenius rxn.duplicate = True - # Set the ID flag to the original rmg index + # Set the ID flag to the original rmg index rxn.ID = str(self.index) else: ct_reaction.reversible = self.reversible @@ -439,6 +462,22 @@ def is_surface_reaction(self): return True return False + def is_charge_transfer_reaction(self): + """ + Return ``True`` if one or more reactants or products are electrons + """ + if self.electrons != 0: + return True + return False + + def is_surface_charge_transfer_reaction(self): + """ + Return ``True`` if one or more reactants or products are electrons + """ + if self.is_surface_reaction() and self.is_charge_transfer_reaction(): + return True + return False + def has_template(self, reactants, products): """ Return ``True`` if the reaction matches the template of `reactants` @@ -506,6 +545,10 @@ def is_isomorphic(self, other, either_direction=True, check_identical=False, che strict=strict, save_order=save_order) + # compare stoichiometry of electrons in reaction + if self.electrons != other.electrons: + return False + # Compare reactants to reactants forward_reactants_match = same_species_lists(self.reactants, other.reactants, check_identical=check_identical, @@ -550,6 +593,33 @@ def is_isomorphic(self, other, either_direction=True, check_identical=False, che # should have already returned if it matches forwards, or we're not allowed to match backwards return reverse_reactants_match and reverse_products_match and collider_match + def _apply_CHE_model(self, T): + """ + Apply the computational hydrogen electrode (CHE) model at temperature T (in 'K'). + + Returns the free energy (in J/mol) of 'N' proton/electron couple(s) in the reaction + using the Reversible Hydrogen Electrode (RHE) as referernce so that + N * deltaG(H+ + e-) = N * 1/2 deltaG(H2(g)) at 0V. + """ + + if not self.is_charge_transfer_reaction(): + raise ReactionError("CHE model is only applicable to charge transfer reactions!") + + if self.electrons != self.protons: + raise ReactionError("Number of electrons must equal number of protons! " + f"{self} has {self.electrons} protons and {self.electrons} electrons") + + + H2_thermo = ThermoData(Tdata=([300,400,500,600,800,1000,1500],'K'), + Cpdata=([6.895,6.975,6.994,7.009,7.081,7.219,7.72],'cal/(mol*K)'), + H298=(0,'kcal/mol'), S298=(31.233,'cal/(mol*K)','+|-',0.0007), + Cp0=(29.1007,'J/(mol*K)'), CpInf=(37.4151,'J/(mol*K)'), + label="""H2""", comment="""Thermo library: primaryThermoLibrary""") + # deltG_H+ + deltaG_e- -> 1/2 deltaG_H2 # only at 298K ??? + + return self.electrons * 0.5 * H2_thermo.get_free_energy(T) + + def get_enthalpy_of_reaction(self, T): """ Return the enthalpy of reaction in J/mol evaluated at temperature @@ -576,12 +646,40 @@ def get_entropy_of_reaction(self, T): dSrxn += product.get_entropy(T) return dSrxn - def get_free_energy_of_reaction(self, T): + def _get_free_energy_of_charge_transfer_reaction(self, T, potential=0.): + + cython.declare(dGrxn=cython.double, reactant=Species, product=Species) + + dGrxn = 0 + for reactant in self.reactants: + try: + dGrxn -= reactant.get_free_energy(T) + except Exception: + logging.error("Problem with reactant {!r} in reaction {!s}".format(reactant, self)) + raise + + for product in self.products: + try: + dGrxn += product.get_free_energy(T) + except Exception: + logging.error("Problem with product {!r} in reaction {!s}".format(reactant, self)) + raise + + if potential != 0.: + dGrxn -= self.electrons * constants.F * potential + + return dGrxn + + def get_free_energy_of_reaction(self, T, potential=0.): """ Return the Gibbs free energy of reaction in J/mol evaluated at - temperature `T` in K. + temperature `T` in K and potential in Volts (if applicable) """ cython.declare(dGrxn=cython.double, reactant=Species, product=Species) + + if self.is_charge_transfer_reaction(): + return self._get_free_energy_of_charge_transfer_reaction(T, potential=potential) + dGrxn = 0.0 for reactant in self.reactants: try: @@ -595,9 +693,33 @@ def get_free_energy_of_reaction(self, T): except Exception: logging.error("Problem with product {!r} in reaction {!s}".format(reactant, self)) raise + return dGrxn - def get_equilibrium_constant(self, T, type='Kc', surface_site_density=2.5e-05): + def get_reversible_potential(self, T): + """ + Get the Potential in `V` at T in 'K' at which the charge transfer reaction is at equilibrium + """ + cython.declare(deltaG=cython.double, V0=cython.double) + if not self.is_charge_transfer_reaction(): + raise KineticsError("Cannot get reversible potential for non charge transfer reactions") + + deltaG = self._get_free_energy_of_charge_transfer_reaction(T) #J/mol + V0 = deltaG / self.electrons / constants.F # V = deltaG / n / F + return V0 + + def set_reference_potential(self, T): + """ + Set the reference Potential of the `SurfaceChargeTransfer` kinetics model to the reversible potential + of the reaction + """ + if self.kinetics is None: + raise KineticsError("Cannot set reference potential for reactions with no kinetics attribute") + + if isinstance(self.kinetics, SurfaceChargeTransfer) and self.kinetics.V0 is None: + self.kinetics.V0 = (self.get_reversible_potential(T),'V') + + def get_equilibrium_constant(self, T, potential=0., type='Kc', surface_site_density=2.5e-05): """ Return the equilibrium constant for the reaction at the specified temperature `T` in K and reference `surface_site_density` @@ -606,16 +728,18 @@ def get_equilibrium_constant(self, T, type='Kc', surface_site_density=2.5e-05): ``Kc`` for concentrations (default), or ``Kp`` for pressures. This function assumes a reference pressure of 1e5 Pa for gas phases species and uses the ideal gas law to determine reference concentrations. For - surface species, the `surface_site_density` is the assumed reference. + surface species, the `surface_site_density` is the assumed reference. For protons (H+), + a reference concentration of 1000 mol/m^3 (1 mol/L) is assumed """ cython.declare(dGrxn=cython.double, K=cython.double, C0=cython.double, P0=cython.double) + cython.declare(dN_gas=cython.int, dN_surf=cython.int, dGrxn=cython.double, K=cython.double, C0=cython.double, P0=cython.double) cython.declare(number_of_gas_reactants=cython.int, number_of_gas_products=cython.int) cython.declare(number_of_surface_reactants=cython.int, number_of_surface_products=cython.int) cython.declare(dN_surf=cython.int, dN_gas=cython.int, sites=cython.int) cython.declare(sigma_nu=cython.double) cython.declare(rectant=Species, product=Species, spcs=Species) # Use free energy of reaction to calculate Ka - dGrxn = self.get_free_energy_of_reaction(T) + dGrxn = self.get_free_energy_of_reaction(T, potential) K = np.exp(-dGrxn / constants.R / T) # Convert Ka to Kc or Kp if specified # Assume a pressure of 1e5 Pa for gas phase species @@ -694,14 +818,14 @@ def get_entropies_of_reaction(self, Tlist): """ return np.array([self.get_entropy_of_reaction(T) for T in Tlist], float) - def get_free_energies_of_reaction(self, Tlist): + def get_free_energies_of_reaction(self, Tlist, potential=0.): """ Return the Gibbs free energies of reaction in J/mol evaluated at temperatures `Tlist` in K. """ - return np.array([self.get_free_energy_of_reaction(T) for T in Tlist], float) + return np.array([self.get_free_energy_of_reaction(T, potential=potential) for T in Tlist], float) - def get_equilibrium_constants(self, Tlist, type='Kc'): + def get_equilibrium_constants(self, Tlist, potential=0., type='Kc'): """ Return the equilibrium constants for the reaction at the specified temperatures `Tlist` in K. The `type` parameter lets you specify the @@ -709,9 +833,7 @@ def get_equilibrium_constants(self, Tlist, type='Kc'): ``Kc`` for concentrations (default), or ``Kp`` for pressures. Note that this function currently assumes an ideal gas mixture. """ - return np.array( - [self.get_equilibrium_constant(T, type) for T in Tlist], float - ) + return np.array([self.get_equilibrium_constant(T, potential=potential, type=type) for T in Tlist], float) def get_stoichiometric_coefficient(self, spec): """ @@ -728,12 +850,12 @@ def get_stoichiometric_coefficient(self, spec): if product is spec: stoich += 1 return stoich - def get_rate_coefficient(self, T, P=0, surface_site_density=0): + def get_rate_coefficient(self, T, P=0, surface_site_density=0, potential=0.): """ Return the overall rate coefficient for the forward reaction at temperature `T` in K and pressure `P` in Pa, including any reaction path degeneracies. - + If diffusion_limiter is enabled, the reaction is in the liquid phase and we use a diffusion limitation to correct the rate. If not, then use the intrinsic rate coefficient. @@ -741,9 +863,11 @@ def get_rate_coefficient(self, T, P=0, surface_site_density=0): If the reaction has sticking coefficient kinetics, a nonzero surface site density in `mol/m^2` must be provided """ - if isinstance(self.kinetics, StickingCoefficient): + if isinstance(self.kinetics,SurfaceChargeTransfer): + return self.get_surface_rate_coefficient(T, surface_site_density=surface_site_density, potential=potential) + elif isinstance(self.kinetics, StickingCoefficient): if surface_site_density <= 0: - raise ValueError("Please provide a postive surface site density in mol/m^2 " + raise ValueError("Please provide a postive surface site density in mol/m^2 " f"for calculating the rate coefficient of {StickingCoefficient.__name__} kinetics") else: return self.get_surface_rate_coefficient(T, surface_site_density) @@ -757,14 +881,15 @@ def get_rate_coefficient(self, T, P=0, surface_site_density=0): else: return self.kinetics.get_rate_coefficient(T, P) - def get_surface_rate_coefficient(self, T, surface_site_density): + def get_surface_rate_coefficient(self, T, surface_site_density, potential=0.): """ Return the overall surface rate coefficient for the forward reaction at temperature `T` in K with surface site density `surface_site_density` in mol/m2. Value is returned in combination of [m,mol,s] """ cython.declare(rateCoefficient=cython.double, - molecularWeight_kg=cython.double, ) + molecularWeight_kg=cython.double, + Ea=cython.double, deltaG=cython.double) if diffusion_limiter.enabled: raise NotImplementedError() @@ -808,6 +933,19 @@ def get_surface_rate_coefficient(self, T, surface_site_density): if isinstance(self.kinetics, SurfaceArrhenius): return self.kinetics.get_rate_coefficient(T, P=0) + if isinstance(self.kinetics, SurfaceChargeTransfer): + Ea = self.kinetics.get_activation_energy_from_potential(potential) + deltaG = self._get_free_energy_of_charge_transfer_reaction(298,potential) + if deltaG > 0 and Ea < deltaG: + corrected_kinetics = deepcopy(self.kinetics) + corrected_kinetics.V0.value_si = potential + corrected_kinetics.Ea.value_si = deltaG + logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol at {3:.2f} V".format( + self, self.kinetics.Ea.value_si / 1000., deltaG / 1000., potential)) + return corrected_kinetics.get_rate_coefficient(T, potential) + else: + return self.kinetics.get_rate_coefficient(T, potential) + raise NotImplementedError("Can't get_surface_rate_coefficient for kinetics type {!r}".format(type(self.kinetics))) def fix_diffusion_limited_a_factor(self, T): @@ -838,22 +976,22 @@ def fix_barrier_height(self, force_positive=False): """ Turns the kinetics into Arrhenius (if they were ArrheniusEP) and ensures the activation energy is at least the endothermicity - for endothermic reactions, and is not negative only as a result + for endothermic reactions, and is not negative only as a result of using Evans Polanyi with an exothermic reaction. If `force_positive` is True, then all reactions are forced to have a non-negative barrier. """ - cython.declare(H0=cython.double, H298=cython.double, Ea=cython.double) + cython.declare(H0=cython.double, H298=cython.double, Ea=cython.double, V0=cython.double, + deltaG=cython.double) if self.kinetics is None: raise KineticsError("Cannot fix barrier height for reactions with no kinetics attribute") - H298 = self.get_enthalpy_of_reaction(298) - H0 = sum([spec.get_thermo_data().E0.value_si if spec.get_thermo_data().E0 is not None else spec.get_thermo_data().to_wilhoit().E0.value_si for spec in self.products]) \ - - sum([spec.get_thermo_data().E0.value_si if spec.get_thermo_data().E0 is not None else spec.get_thermo_data().to_wilhoit().E0.value_si for spec in self.reactants]) - if isinstance(self.kinetics, (ArrheniusEP, SurfaceArrheniusBEP, StickingCoefficientBEP, ArrheniusBM)): + if isinstance(self.kinetics, SurfaceChargeTransferBEP): Ea = self.kinetics.E0.value_si # temporarily using Ea to store the intrinsic barrier height E0 - self.kinetics = self.kinetics.to_arrhenius(H298) + V0 = self.kinetics.V0.value_si + deltaG = self._get_free_energy_of_charge_transfer_reaction(298,V0) + self.kinetics = self.kinetics.to_surface_charge_transfer(deltaG) if self.kinetics.Ea.value_si < 0.0 and self.kinetics.Ea.value_si < Ea: # Calculated Ea (from Evans-Polanyi) is negative AND below than the intrinsic E0 Ea = min(0.0, Ea) # (the lowest we want it to be) @@ -862,33 +1000,48 @@ def fix_barrier_height(self, force_positive=False): logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol.".format( self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) self.kinetics.Ea.value_si = Ea - if isinstance(self.kinetics, (Arrhenius, StickingCoefficient)): # SurfaceArrhenius is a subclass of Arrhenius - Ea = self.kinetics.Ea.value_si - if H0 >= 0 and Ea < H0: - self.kinetics.Ea.value_si = H0 - self.kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol to match endothermicity of " \ - "reaction.".format( Ea / 1000., H0 / 1000.) - logging.info("For reaction {2!s}, Ea raised from {0:.1f} to {1:.1f} kJ/mol to match " - "endothermicity of reaction.".format( Ea / 1000., H0 / 1000., self)) - if force_positive and isinstance(self.kinetics, (Arrhenius, StickingCoefficient)) and self.kinetics.Ea.value_si < 0: - self.kinetics.comment += "\nEa raised from {0:.1f} to 0 kJ/mol.".format(self.kinetics.Ea.value_si / 1000.) - logging.info("For reaction {1!s} Ea raised from {0:.1f} to 0 kJ/mol.".format( - self.kinetics.Ea.value_si / 1000., self)) - self.kinetics.Ea.value_si = 0 - if self.kinetics.is_pressure_dependent() and self.network_kinetics is not None: - Ea = self.network_kinetics.Ea.value_si - if H0 >= 0 and Ea < H0: - self.network_kinetics.Ea.value_si = H0 - self.network_kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol to match endothermicity of" \ - " reaction.".format(Ea / 1000., H0 / 1000.) - logging.info("For reaction {2!s}, Ea of the high pressure limit kinetics raised from {0:.1f} to {1:.1f}" - " kJ/mol to match endothermicity of reaction.".format(Ea / 1000., H0 / 1000., self)) - if force_positive and isinstance(self.kinetics, Arrhenius) and self.kinetics.Ea.value_si < 0: - self.network_kinetics.comment += "\nEa raised from {0:.1f} to 0 kJ/mol.".format( - self.kinetics.Ea.value_si / 1000.) - logging.info("For reaction {1!s} Ea of the high pressure limit kinetics raised from {0:.1f} to 0" - " kJ/mol.".format(self.kinetics.Ea.value_si / 1000., self)) + else: + H298 = self.get_enthalpy_of_reaction(298) + H0 = sum([spec.get_thermo_data().E0.value_si if spec.get_thermo_data().E0 is not None else spec.get_thermo_data().to_wilhoit().E0.value_si for spec in self.products]) \ + - sum([spec.get_thermo_data().E0.value_si if spec.get_thermo_data().E0 is not None else spec.get_thermo_data().to_wilhoit().E0.value_si for spec in self.reactants]) + if isinstance(self.kinetics, (ArrheniusEP, SurfaceArrheniusBEP, StickingCoefficientBEP, ArrheniusBM)): + Ea = self.kinetics.E0.value_si # temporarily using Ea to store the intrinsic barrier height E0 + self.kinetics = self.kinetics.to_arrhenius(H298) + if self.kinetics.Ea.value_si < 0.0 and self.kinetics.Ea.value_si < Ea: + # Calculated Ea (from Evans-Polanyi) is negative AND below than the intrinsic E0 + Ea = min(0.0, Ea) # (the lowest we want it to be) + self.kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol.".format( + self.kinetics.Ea.value_si / 1000., Ea / 1000.) + logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol.".format( + self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) + self.kinetics.Ea.value_si = Ea + if isinstance(self.kinetics, (Arrhenius, StickingCoefficient)): # SurfaceArrhenius is a subclass of Arrhenius + Ea = self.kinetics.Ea.value_si + if H0 >= 0 and Ea < H0: + self.kinetics.Ea.value_si = H0 + self.kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol to match endothermicity of " \ + "reaction.".format( Ea / 1000., H0 / 1000.) + logging.info("For reaction {2!s}, Ea raised from {0:.1f} to {1:.1f} kJ/mol to match " + "endothermicity of reaction.".format( Ea / 1000., H0 / 1000., self)) + if force_positive and isinstance(self.kinetics, (Arrhenius, StickingCoefficient)) and self.kinetics.Ea.value_si < 0: + self.kinetics.comment += "\nEa raised from {0:.1f} to 0 kJ/mol.".format(self.kinetics.Ea.value_si / 1000.) + logging.info("For reaction {1!s} Ea raised from {0:.1f} to 0 kJ/mol.".format( + self.kinetics.Ea.value_si / 1000., self)) self.kinetics.Ea.value_si = 0 + if self.kinetics.is_pressure_dependent() and self.network_kinetics is not None: + Ea = self.network_kinetics.Ea.value_si + if H0 >= 0 and Ea < H0: + self.network_kinetics.Ea.value_si = H0 + self.network_kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol to match endothermicity of" \ + " reaction.".format(Ea / 1000., H0 / 1000.) + logging.info("For reaction {2!s}, Ea of the high pressure limit kinetics raised from {0:.1f} to {1:.1f}" + " kJ/mol to match endothermicity of reaction.".format(Ea / 1000., H0 / 1000., self)) + if force_positive and isinstance(self.kinetics, Arrhenius) and self.kinetics.Ea.value_si < 0: + self.network_kinetics.comment += "\nEa raised from {0:.1f} to 0 kJ/mol.".format( + self.kinetics.Ea.value_si / 1000.) + logging.info("For reaction {1!s} Ea of the high pressure limit kinetics raised from {0:.1f} to 0" + " kJ/mol.".format(self.kinetics.Ea.value_si / 1000., self)) + self.kinetics.Ea.value_si = 0 def reverse_arrhenius_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): """ @@ -961,16 +1114,42 @@ def reverse_sticking_coeff_rate(self, k_forward, reverse_units, surface_site_den kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) return kr + def reverse_surface_charge_transfer_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): + """ + Reverses the given k_forward, which must be a SurfaceChargeTransfer type. + You must supply the correct units for the reverse rate. + The equilibrium constant is evaluated from the current reaction instance (self). + """ + cython.declare(kf=SurfaceChargeTransfer, kr=SurfaceChargeTransfer) + cython.declare(Tlist=np.ndarray, klist=np.ndarray, i=cython.int, V0=cython.double) + kf = k_forward + self.set_reference_potential(298) + if not isinstance(kf, SurfaceChargeTransfer): # Only reverse SurfaceChargeTransfer rates + raise TypeError(f'Expected a SurfaceChargeTransfer object for k_forward but received {kf}') + if Tmin is not None and Tmax is not None: + Tlist = 1.0 / np.linspace(1.0 / Tmax.value, 1.0 / Tmin.value, 50) + else: + Tlist = np.linspace(298, 500, 30) + + V0 = self.kinetics.V0.value_si + klist = np.zeros_like(Tlist) + for i in range(len(Tlist)): + klist[i] = kf.get_rate_coefficient(Tlist[i],V0) / self.get_equilibrium_constant(Tlist[i],V0) + kr = SurfaceChargeTransfer(alpha=1-kf.alpha.value, electrons=-1*self.electrons, V0=(V0,'V')) + kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + return kr + def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, Tmax=None, surface_site_density=0): """ - Generate and return a rate coefficient model for the reverse reaction. + Generate and return a rate coefficient model for the reverse reaction. Currently this only works if the `kinetics` attribute is one of several (but not necessarily all) kinetics types. If the reaction kinetics model is Sticking Coefficient, please provide a nonzero surface site density in `mol/m^2` which is required to evaluate the rate coefficient. """ - cython.declare(Tlist=np.ndarray, Plist=np.ndarray, K=np.ndarray, + cython.declare(n_gas=cython.int, n_surf=cython.int, prod=Species, k_units=str, + Tlist=np.ndarray, Plist=np.ndarray, K=np.ndarray, rxn=Reaction, klist=np.ndarray, i=cython.size_t, Tindex=cython.size_t, Pindex=cython.size_t) @@ -978,6 +1157,7 @@ def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, T KineticsData.__name__, Arrhenius.__name__, SurfaceArrhenius.__name__, + SurfaceChargeTransfer.__name__, MultiArrhenius.__name__, PDepArrhenius.__name__, MultiPDepArrhenius.__name__, @@ -993,15 +1173,19 @@ def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, T surf_prods = [spcs for spcs in self.products if spcs.contains_surface_site()] except IndexError: surf_prods = [] - # logging.warning(f"Species do not have an rmgpy.molecule.Molecule " - # "Cannot determine phases of species. We will assume gas" - # ) + logging.warning(f"Species do not have an rmgpy.molecule.Molecule " + "Cannot determine phases of species. We will assume gas" + ) n_surf = len(surf_prods) n_gas = len(self.products) - len(surf_prods) kunits = get_rate_coefficient_units_from_reaction_order(n_gas, n_surf) kf = self.kinetics - if isinstance(kf, KineticsData): + + if isinstance(kf, SurfaceChargeTransfer): + return self.reverse_surface_charge_transfer_rate(kf, kunits, Tmin, Tmax) + + elif isinstance(kf, KineticsData): Tlist = kf.Tdata.value_si klist = np.zeros_like(Tlist) @@ -1020,7 +1204,7 @@ def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, T elif isinstance(kf, StickingCoefficient): if surface_site_density <= 0: - raise ValueError("Please provide a postive surface site density in mol/m^2 " + raise ValueError("Please provide a postive surface site density in mol/m^2 " f"for calculating the rate coefficient of {StickingCoefficient.__name__} kinetics") else: return self.reverse_sticking_coeff_rate(kf, kunits, surface_site_density, Tmin, Tmax) @@ -1149,14 +1333,14 @@ def calculate_microcanonical_rate_coefficient(self, e_list, j_list, reac_dens_st reactant density of states is required; if the reaction is reversible, then both are required. This function will try to use the best method that it can based on the input data available: - + * If detailed information has been provided for the transition state (i.e. the molecular degrees of freedom), then RRKM theory will be used. - + * If the above is not possible but high-pressure limit kinetics - :math:`k_\\infty(T)` have been provided, then the inverse Laplace + :math:`k_\\infty(T)` have been provided, then the inverse Laplace transform method will be used. - + The density of states for the product `prod_dens_states` and the temperature of interest `T` in K can also be provided. For isomerization and association reactions `prod_dens_states` is required; for dissociation reactions it is @@ -1174,10 +1358,13 @@ def is_balanced(self): from rmgpy.molecule.element import element_list from rmgpy.molecule.fragment import CuttingLabel, Fragment - cython.declare(reactant_elements=dict, product_elements=dict, molecule=Graph, atom=Vertex, element=Element) + cython.declare(reactant_elements=dict, product_elements=dict, molecule=Graph, atom=Vertex, element=Element, + reactants_net_charge=cython.int, products_net_charge=cython.int) reactant_elements = {} product_elements = {} + reactants_net_charge = 0 + products_net_charge = 0 for element in element_list: reactant_elements[element] = 0 product_elements[element] = 0 @@ -1187,34 +1374,48 @@ def is_balanced(self): molecule = reactant.molecule[0] for atom in molecule.atoms: if not isinstance(atom, CuttingLabel): + reactants_net_charge += atom.charge reactant_elements[atom.element] += 1 elif isinstance(reactant, Molecule): molecule = reactant for atom in molecule.atoms: + reactants_net_charge += atom.charge reactant_elements[atom.element] += 1 elif isinstance(reactant, Fragment): for atom in reactant.atoms: if not isinstance(atom, CuttingLabel): + reactants_net_charge += atom.charge reactant_elements[atom.element] += 1 for product in self.products: if isinstance(product, Species): molecule = product.molecule[0] for atom in molecule.atoms: if not isinstance(atom, CuttingLabel): + products_net_charge += atom.charge product_elements[atom.element] += 1 elif isinstance(product, Molecule): molecule = product for atom in molecule.atoms: + products_net_charge += atom.charge product_elements[atom.element] += 1 elif isinstance(product, Fragment): for atom in product.atoms: if not isinstance(atom, CuttingLabel): + products_net_charge += atom.charge product_elements[atom.element] += 1 for element in element_list: if reactant_elements[element] != product_elements[element]: return False + if self.electrons < 0: + reactants_net_charge += self.electrons + elif self.electrons > 0: + products_net_charge -= self.electrons + + if reactants_net_charge != products_net_charge: + return False + return True def generate_pairs(self): @@ -1222,7 +1423,7 @@ def generate_pairs(self): Generate the reactant-product pairs to use for this reaction when performing flux analysis. The exact procedure for doing so depends on the reaction type: - + =================== =============== ======================================== Reaction type Template Resulting pairs =================== =============== ======================================== @@ -1231,8 +1432,8 @@ def generate_pairs(self): Association A + B -> C (A,C), (B,C) Bimolecular A + B -> C + D (A,C), (B,D) *or* (A,D), (B,C) =================== =============== ======================================== - - There are a number of ways of determining the correct pairing for + + There are a number of ways of determining the correct pairing for bimolecular reactions. Here we try a simple similarity analysis by comparing the number of heavy atoms. This should work most of the time, but a more rigorous algorithm may be needed for some cases. @@ -1295,9 +1496,9 @@ def _repr_png_(self): # Build the transition state geometry def generate_3d_ts(self, reactants, products): """ - Generate the 3D structure of the transition state. Called from + Generate the 3D structure of the transition state. Called from model.generate_kinetics(). - + self.reactants is a list of reactants self.products is a list of products """ @@ -1307,7 +1508,7 @@ def generate_3d_ts(self, reactants, products): atoms involved in the reaction. If a radical is involved, can find the atom with radical electrons. If a more reliable method can be found, would greatly improve the method. - + Repeat for the products """ for i in range(0, len(reactants)): From 476a7145dda301696ee31a432799f9f405e26a56 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 16:29:39 -0400 Subject: [PATCH 017/109] adding charged species and electrons to family.py --- rmgpy/data/kinetics/family.py | 95 +++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index f8818c78ac..96d30d312d 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -55,7 +55,7 @@ from rmgpy.exceptions import ActionError, DatabaseError, InvalidActionError, KekulizationError, KineticsError, \ ForbiddenStructureException, UndeterminableKineticsError from rmgpy.kinetics import Arrhenius, SurfaceArrhenius, SurfaceArrheniusBEP, StickingCoefficient, \ - StickingCoefficientBEP, ArrheniusBM + StickingCoefficientBEP, ArrheniusBM, SurfaceChargeTransfer from rmgpy.kinetics.uncertainties import RateUncertainty, rank_accuracy_map from rmgpy.molecule import Bond, GroupBond, Group, Molecule from rmgpy.molecule.atomtype import ATOMTYPES @@ -102,6 +102,7 @@ def __init__(self, estimator=None, reverse=None, is_forward=None, + electrons=0, ): Reaction.__init__(self, index=index, @@ -115,6 +116,7 @@ def __init__(self, degeneracy=degeneracy, pairs=pairs, is_forward=is_forward, + electrons=electrons ) self.family = family self.template = template @@ -140,7 +142,8 @@ def __reduce__(self): self.template, self.estimator, self.reverse, - self.is_forward + self.is_forward, + self.electrons )) def __repr__(self): @@ -162,6 +165,7 @@ def __repr__(self): if self.pairs is not None: string += 'pairs={0}, '.format(self.pairs) if self.family: string += "family='{}', ".format(self.family) if self.template: string += "template={}, ".format(self.template) + if self.electrons: string += "electrons={}, ".format(self.electrons) if self.comment != '': string += 'comment={0!r}, '.format(self.comment) string = string[:-2] + ')' return string @@ -195,6 +199,7 @@ def copy(self): other.transition_state = deepcopy(self.transition_state) other.duplicate = self.duplicate other.pairs = deepcopy(self.pairs) + other.electrons = self.electrons # added for TemplateReaction information other.family = self.family @@ -260,6 +265,10 @@ def get_reverse(self): other.add_action(['GAIN_RADICAL', action[1], action[2]]) elif action[0] == 'GAIN_RADICAL': other.add_action(['LOSE_RADICAL', action[1], action[2]]) + elif action[0] == 'GAIN_CHARGE': + other.add_action(['LOSE_CHARGE', action[1], action[2]]) + elif action[0] == 'LOSE_CHARGE': + other.add_action(['GAIN_CHARGE', action[1], action[2]]) elif action[0] == 'LOSE_PAIR': other.add_action(['GAIN_PAIR', action[1], action[2]]) elif action[0] == 'GAIN_PAIR': @@ -383,7 +392,7 @@ def _apply(self, struct, forward, unique): atom1.apply_action(['BREAK_BOND', label1, info, label2]) atom2.apply_action(['BREAK_BOND', label1, info, label2]) - elif action[0] in ['LOSE_RADICAL', 'GAIN_RADICAL']: + elif action[0] in ['LOSE_RADICAL', 'GAIN_RADICAL', 'LOSE_CHARGE', 'GAIN_CHARGE']: label, change = action[1:] change = int(change) @@ -401,6 +410,10 @@ def _apply(self, struct, forward, unique): atom.apply_action(['GAIN_RADICAL', label, 1]) elif (action[0] == 'LOSE_RADICAL' and forward) or (action[0] == 'GAIN_RADICAL' and not forward): atom.apply_action(['LOSE_RADICAL', label, 1]) + elif (action[0] == 'LOSE_CHARGE' and forward) or (action[0] == 'GAIN_CHARGE' and not forward): + atom.apply_action(['LOSE_CHARGE', label, 1]) + elif (action[0] == 'GAIN_CHARGE' and forward) or (action[0] == 'LOSE_CHARGE' and not forward): + atom.apply_action(['GAIN_CHARGE', label, 1]) elif action[0] in ['LOSE_PAIR', 'GAIN_PAIR']: @@ -558,6 +571,8 @@ def load(self, path, local_context=None, global_context=None, depository_labels= local_context['reactantNum'] = None local_context['productNum'] = None local_context['autoGenerated'] = False + local_context['allowChargedSpecies'] = False + local_context['electrons'] = 0 self.groups = KineticsGroups(label='{0}/groups'.format(self.label)) logging.debug("Loading kinetics family groups from {0}".format(os.path.join(path, 'groups.py'))) Database.load(self.groups, os.path.join(path, 'groups.py'), local_context, global_context) @@ -570,6 +585,8 @@ def load(self, path, local_context=None, global_context=None, depository_labels= self.product_num = local_context.get('productNum', None) self.auto_generated = local_context.get('autoGenerated', False) + self.allow_charged_species = local_context.get('allowChargedSpecies', False) + self.electrons = local_context.get('electrons', 0) if self.reactant_num: self.groups.reactant_num = self.reactant_num @@ -673,7 +690,8 @@ def load_recipe(self, actions): for action in actions: action[0] = action[0].upper() valid_actions = [ - 'CHANGE_BOND', 'FORM_BOND', 'BREAK_BOND', 'GAIN_RADICAL', 'LOSE_RADICAL', 'GAIN_PAIR', 'LOSE_PAIR' + 'CHANGE_BOND', 'FORM_BOND', 'BREAK_BOND', 'GAIN_RADICAL', 'LOSE_RADICAL', + 'GAIN_CHARGE', 'LOSE_CHARGE', 'GAIN_PAIR', 'LOSE_PAIR' ] if action[0] not in valid_actions: raise InvalidActionError('Action {0} is not a recognized action. ' @@ -865,6 +883,12 @@ def save_groups(self, path): if self.auto_generated is not None: f.write('autoGenerated = {0}\n\n'.format(self.auto_generated)) + if self.allow_charged_species: + f.write('allowChargedSpecies = {0}\n\n'.format(self.allow_charged_species)) + + if self.electrons != 0: + f.write('electrons = {0}\n\n'.format(self.electrons)) + # Write the recipe f.write('recipe(actions=[\n') for action in self.forward_recipe.actions: @@ -1076,6 +1100,21 @@ def add_rules_from_training(self, thermo_database=None, train_indices=None): Tmax=deepcopy(data.Tmax), coverage_dependence=deepcopy(data.coverage_dependence), ) + elif isinstance(data, SurfaceChargeTransfer): + for reactant in entry.item.reactants: + # Clear atom labels to avoid effects on thermo generation, ok because this is a deepcopy + reactant_copy = reactant.copy(deep=True) + reactant_copy.molecule[0].clear_labeled_atoms() + reactant_copy.generate_resonance_structures() + reactant.thermo = thermo_database.get_thermo_data(reactant_copy, training_set=True) + for product in entry.item.products: + product_copy = product.copy(deep=True) + product_copy.molecule[0].clear_labeled_atoms() + product_copy.generate_resonance_structures() + product.thermo = thermo_database.get_thermo_data(product_copy, training_set=True) + V = data.V0.value_si + dGrxn = entry.item._get_free_energy_of_charge_transfer_reaction(298,V) + data = data.to_surface_charge_transfer_bep(dGrxn,0.0) else: raise NotImplementedError("Unexpected training kinetics type {} for {}".format(type(data), entry)) @@ -1104,7 +1143,9 @@ def add_rules_from_training(self, thermo_database=None, train_indices=None): for entry in reverse_entries: tentries[entry.index].item.is_forward = False - assert isinstance(entry.data, Arrhenius) + if not isinstance(entry.data, Arrhenius): + print(self.label) + assert False data = deepcopy(entry.data) data.change_t0(1) # Estimate the thermo for the reactants and products @@ -1383,15 +1424,19 @@ def apply_recipe(self, reactant_structures, forward=True, unique=True, relabel_a struc.update() reactant_net_charge += struc.get_net_charge() + + is_molecule = True for struct in product_structures: # If product structures are Molecule objects, update their atom types # If product structures are Group objects and the reaction is in certain families # (families with charged substances), the charge of structures will be updated if isinstance(struct, Molecule): + struct.update_charge() struct.update(sort_atoms=not self.save_order) elif isinstance(struct, Fragment): struct.update() elif isinstance(struct, Group): + is_molecule = False struct.reset_ring_membership() if label in ['1,2_insertion_co', 'r_addition_com', 'co_disproportionation', 'intra_no2_ono_conversion', 'lone_electron_pair_bond', @@ -1399,20 +1444,25 @@ def apply_recipe(self, reactant_structures, forward=True, unique=True, relabel_a struct.update_charge() else: raise TypeError('Expecting Molecule or Group object, not {0}'.format(struct.__class__.__name__)) - product_net_charge += struc.get_net_charge() - if reactant_net_charge != product_net_charge: + product_net_charge += struct.get_net_charge() + + + if self.electrons < 0: + if forward: + reactant_net_charge += self.electrons + else: + product_net_charge += self.electrons + elif self.electrons > 0: + if forward: + product_net_charge -= self.electrons + else: + reactant_net_charge -= self.electrons + + if reactant_net_charge != product_net_charge and is_molecule: logging.debug( 'The net charge of the reactants {0} differs from the net charge of the products {1} in reaction ' 'family {2}. Not generating this reaction.'.format(reactant_net_charge, product_net_charge, self.label)) return None - # The following check should be removed once RMG can process charged species - # This is applied only for :class:Molecule (not for :class:Group which is allowed to have a nonzero net charge) - if any([structure.get_net_charge() for structure in reactant_structures + product_structures]) \ - and isinstance(struc, Molecule): - logging.debug( - 'A net charged species was formed when reacting {0} to form {1} in reaction family {2}. Not generating ' - 'this reaction.'.format(reactant_net_charge, product_net_charge, self.label)) - return None # If there are two product structures, place the one containing '*1' first if len(product_structures) == 2: @@ -1558,8 +1608,17 @@ def _create_reaction(self, reactants, products, is_forward): reversible=self.reversible, family=self.label, is_forward=is_forward, + electrons = self.electrons ) + if not self.allow_charged_species: + charged_species = [spc for spc in (reaction.reactants + reaction.products) if spc.get_net_charge() != 0] + if charged_species: + return None + + if not reaction.is_balanced(): + return None + # Store the labeled atoms so we can recover them later # (e.g. for generating reaction pairs and templates) for key, species_list in zip(['reactants', 'products'], [reaction.reactants, reaction.products]): @@ -1749,6 +1808,12 @@ def calculate_degeneracy(self, reaction, resonance=True): This method by default adjusts for double counting of identical reactants. This should only be adjusted once per reaction. """ + # Check if the reactants are the same + # If they refer to the same memory address, then make a deep copy so + # they can be manipulated independently + if reaction.is_charge_transfer_reaction(): + # Not implemented yet for charge transfer reactions + return 1 reactants = reaction.reactants reactants, same_reactants = check_for_same_reactants(reactants) From 90ff67b81633c9a5d7b3b873996f0b226c7307b7 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 13:13:54 -0400 Subject: [PATCH 018/109] added group unit tests --- test/rmgpy/molecule/groupTest.py | 118 +++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/test/rmgpy/molecule/groupTest.py b/test/rmgpy/molecule/groupTest.py index b980621d0c..3c1fdb3dae 100644 --- a/test/rmgpy/molecule/groupTest.py +++ b/test/rmgpy/molecule/groupTest.py @@ -228,6 +228,66 @@ def test_apply_action_lose_radical(self): atom1.apply_action(action) assert atom1.radical_electrons == [0, 1, 2, 3] + def test_apply_action_gain_charge(self): + """ + Test the GroupAtom.apply_action() method for a GAIN_CHARGE action. + """ + action = ['GAIN_CHARGE', '*1', 1] + for label, atomtype in ATOMTYPES.items(): + atom0 = GroupAtom(atomtype=[atomtype], radical_electrons=[0], charge=[0], label='*1', lone_pairs=[0]) + atom = atom0.copy() + try: + atom.apply_action(action) + self.assertEqual(len(atom.atomtype), len(atomtype.increment_charge)) + for a in atomtype.increment_charge: + self.assertTrue(a in atom.atomtype, + "GAIN_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, + atomtype.increment_charge)) + # self.assertEqual(atom0.radical_electrons, [r + 1 for r in atom.radical_electrons]) + self.assertEqual(atom0.charge, [c - 1 for c in atom.charge]) + self.assertEqual(atom0.label, atom.label) + self.assertEqual(atom0.lone_pairs, atom.lone_pairs) + except ActionError: + self.assertEqual(len(atomtype.increment_charge), 0) + + # test when radicals unspecified + group = Group().from_adjacency_list(""" + 1 R ux + """) # ux causes a wildcard for radicals + atom1 = group.atoms[0] + atom1.apply_action(action) + #self.assertListEqual(atom1.radical_electrons, [0, 1, 2, 3]) + + def test_apply_action_lose_charge(self): + """ + Test the GroupAtom.apply_action() method for a LOSE_CHARGE action. + """ + action = ['LOSE_CHARGE', '*1', 1] + for label, atomtype in ATOMTYPES.items(): + atom0 = GroupAtom(atomtype=[atomtype], radical_electrons=[1], charge=[0], label='*1', lone_pairs=[0]) + atom = atom0.copy() + try: + atom.apply_action(action) + self.assertEqual(len(atom.atomtype), len(atomtype.decrement_charge)) + for a in atomtype.decrement_charge: + self.assertTrue(a in atom.atomtype, + "LOSE_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, + atomtype.decrement_charge)) + # self.assertEqual(atom0.radical_electrons, [r - 1 for r in atom.radical_electrons]) + self.assertEqual(atom0.charge, [c + 1 for c in atom.charge]) + self.assertEqual(atom0.label, atom.label) + self.assertEqual(atom0.lone_pairs, atom.lone_pairs) + except ActionError: + self.assertEqual(len(atomtype.decrement_charge), 0) + + # test when radicals unspecified + group = Group().from_adjacency_list(""" + 1 R ux + """) # ux causes a wildcard for radicals + atom1 = group.atoms[0] + atom1.apply_action(action) + #self.assertListEqual(atom1.radical_electrons, [1, 2, 3, 4]) + def test_apply_action_gain_pair(self): """ Test the GroupAtom.apply_action() method for a GAIN_PAIR action when lone_pairs is either specified or not. @@ -482,6 +542,19 @@ def test_make_sample_atom(self): assert new_atom.charge == 0 assert new_atom.lone_pairs == 0 + def test_is_electron(self): + """ + Test the GroupAtom.is_electron() method. + """ + electron = GroupAtom(atomtype=[ATOMTYPES['e']]) + self.assertTrue(electron.is_electron()) + + def test_is_proton(self): + """ + Test the GroupAtom.is_proton() method. + """ + proton = GroupAtom(atomtype=[ATOMTYPES['H+']]) + self.assertTrue(proton.is_proton()) class TestGroupBond: """ @@ -666,6 +739,35 @@ def test_apply_action_lose_radical(self): except ActionError: pass + + def test_apply_action_gain_charge(self): + """ + Test the GroupBond.apply_action() method for a GAIN_RADICAL action. + """ + action = ['GAIN_CHARGE', '*1', 1] + for order0 in self.orderList: + bond0 = GroupBond(None, None, order=order0) + bond = bond0.copy() + try: + bond.apply_action(action) + self.fail('GroupBond.apply_action() unexpectedly processed a GAIN_CHARGE action.') + except ActionError: + pass + + def test_apply_action_lose_charge(self): + """ + Test the GroupBond.apply_action() method for a LOSE_CHARGE action. + """ + action = ['LOSE_CHARGE', '*1', 1] + for order0 in self.orderList: + bond0 = GroupBond(None, None, order=order0) + bond = bond0.copy() + try: + bond.apply_action(action) + self.fail('GroupBond.apply_action() unexpectedly processed a LOSE_CHARGE action.') + except ActionError: + pass + def test_equivalent(self): """ Test the GroupBond.equivalent() method. @@ -775,6 +877,22 @@ def test_is_surface_site(self): surface_site = Group().from_adjacency_list("1 *1 X u0") assert surface_site.is_surface_site() + def test_is_electron(self): + """ + Test the Group.is_electron() method. + """ + self.assertFalse(self.group.is_electron()) + electron = Group().from_adjacency_list("""1 *1 e u1 p0 c-1""") + self.assertTrue(electron.is_electron()) + + def test_is_proton(self): + """ + Test the Group.is_proton() method. + """ + self.assertFalse(self.group.is_proton()) + proton = Group().from_adjacency_list("""1 *1 H+ u0 p0 c+1""") + self.assertTrue(proton.is_proton()) + def test_get_labeled_atom(self): """ Test the Group.get_labeled_atoms() method. From 1924cc294b1e1f9a0bc647f272935b00ce5f5395 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 15:02:03 -0400 Subject: [PATCH 019/109] added Molecule unit tests --- test/rmgpy/molecule/moleculeTest.py | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/rmgpy/molecule/moleculeTest.py b/test/rmgpy/molecule/moleculeTest.py index 33e75dec58..d30d14ccd4 100644 --- a/test/rmgpy/molecule/moleculeTest.py +++ b/test/rmgpy/molecule/moleculeTest.py @@ -113,6 +113,31 @@ def test_is_hydrogen(self): else: assert not atom.is_hydrogen() + def test_is_proton(self): + """ + Test the Atom.is_proton() method. + """ + for element in element_list: + atom = Atom(element=element, radical_electrons=0, charge=1, label='*1', lone_pairs=0) + if element.symbol == 'H': + self.assertTrue(atom.is_hydrogen()) + self.assertTrue(atom.is_proton()) + atom.charge = 0 + self.assertFalse(atom.is_proton()) + else: + self.assertFalse(atom.is_proton()) + + def test_is_electron(self): + """ + Test the Atom.is_electron() method. + """ + for element in element_list: + atom = Atom(element=element, radical_electrons=1, charge=-1, label='*1', lone_pairs=0) + if element.symbol == 'e': + self.assertTrue(atom.is_electron()) + else: + self.assertFalse(atom.is_electron()) + def test_is_non_hydrogen(self): """ Test the Atom.is_non_hydrogen() method. @@ -396,6 +421,34 @@ def test_apply_action_lose_radical(self): assert atom0.charge == atom.charge assert atom0.label == atom.label + def test_apply_action_gain_charge(self): + """ + Test the Atom.apply_action() method for a GAIN_CHARGE action. + """ + action = ['GAIN_CHARGE', '*1', 1] + for element in element_list: + atom0 = Atom(element=element, radical_electrons=0, charge=0, label='*1', lone_pairs=0) + atom = atom0.copy() + atom.apply_action(action) + self.assertEqual(atom0.element, atom.element) + # self.assertEqual(atom0.radical_electrons, atom.radical_electrons + 1) + self.assertEqual(atom0.charge, atom.charge - 1) + self.assertEqual(atom0.label, atom.label) + + def test_apply_action_lose_charge(self): + """ + Test the Atom.apply_action() method for a LOSE_CHARGE action. + """ + action = ['LOSE_CHARGE', '*1', 1] + for element in element_list: + atom0 = Atom(element=element, radical_electrons=0, charge=0, label='*1', lone_pairs=0) + atom = atom0.copy() + atom.apply_action(action) + self.assertEqual(atom0.element, atom.element) + # self.assertEqual(atom0.radical_electrons, atom.radical_electrons - 1) + self.assertEqual(atom0.charge, atom.charge + 1) + self.assertEqual(atom0.label, atom.label) + def test_equivalent(self): """ Test the Atom.equivalent() method. @@ -946,6 +999,18 @@ def setup_method(self): self.mol2 = Molecule(smiles="C") self.mol3 = Molecule(smiles="CC") + def test_is_proton(self): + """Test the Molecule `is_proton()` method""" + proton = Molecule().from_adjacency_list("""1 H u0 c+1""") + hydrogen = Molecule().from_adjacency_list("""1 H u1""") + self.assertTrue(proton.is_proton()) + self.assertFalse(hydrogen.is_proton()) + + def test_is_electron(self): + """Test the Molecule `is_electron()` method""" + electron = Molecule().from_adjacency_list("""1 e u1 c-1""") + self.assertTrue(electron.is_electron()) + def test_equality(self): """Test that we can perform equality comparison with Molecule objects""" assert self.mol1 == self.mol1 From 9bf64aa7ee51c0b7741b205a4424723f8bd46588 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 15:16:33 -0400 Subject: [PATCH 020/109] added reaction tests for charge transfer reactions port the new reaction tests to the new testing file and style by Jackson Burns Date: Thu Feb 01 2024 10:47:18 GMT-0500 (Eastern Standard Time) Co-authored-by: davidfarinajr Co-authored-by: Jackson Burns --- test/rmgpy/reactionTest.py | 202 +++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/test/rmgpy/reactionTest.py b/test/rmgpy/reactionTest.py index 26441c9e78..4b8a0fbac1 100644 --- a/test/rmgpy/reactionTest.py +++ b/test/rmgpy/reactionTest.py @@ -31,6 +31,7 @@ This module contains unit tests of the rmgpy.reaction module. """ +import math import cantera as ct import numpy @@ -52,6 +53,7 @@ Chebyshev, SurfaceArrhenius, StickingCoefficient, + SurfaceChargeTransfer, ) from rmgpy.molecule import Molecule from rmgpy.quantity import Quantity @@ -64,6 +66,8 @@ from rmgpy.statmech.vibration import HarmonicOscillator from rmgpy.thermo import Wilhoit, ThermoData, NASA, NASAPolynomial +def order_of_magnitude(number): + return math.floor(math.log(number, 10)) class PseudoSpecies(object): """ @@ -436,6 +440,16 @@ def setup_class(self): comment="""Approximate rate""", ), ) + + def test_electrons(self): + """Test electrons property""" + self.assertEquals(self.rxn1s.electrons,0) + self.assertEquals(self.rxn1s.electrons,0) + + def test_protons(self): + """Test protons property""" + self.assertEquals(self.rxn1s.protons,0) + self.assertEquals(self.rxn1s.protons,0) def test_is_surface_reaction_species(self): """Test is_surface_reaction for reaction based on Species""" @@ -2996,3 +3010,191 @@ def test_falloff(self): assert ct_lindemann.efficiencies == self.ct_lindemann.efficiencies assert str(ct_lindemann.rate.low_rate) == str(self.ct_lindemann.rate.low_rate) assert str(ct_lindemann.rate.high_rate) == str(self.ct_lindemann.rate.high_rate) + + +class TestChargeTransferReaction: + """Test charge transfer reactions""" + + def setup_class(self): + m_electron = Molecule().from_smiles("e") + m_proton = Molecule().from_smiles("[H+]") + m_x = Molecule().from_adjacency_list("1 X u0 p0") + m_ch2x = Molecule().from_adjacency_list( + """ + 1 C u0 p0 c0 {2,S} {3,S} {4,D} + 2 H u0 p0 c0 {1,S} + 3 H u0 p0 c0 {1,S} + 4 X u0 p0 c0 {1,D} + """ + ) + m_ch3x = Molecule().from_adjacency_list( + """ + 1 C u0 p0 c0 {2,S} {3,S} {4,S} {5,S} + 2 H u0 p0 c0 {1,S} + 3 H u0 p0 c0 {1,S} + 4 H u0 p0 c0 {1,S} + 5 X u0 p0 c0 {1,S} + """ + ) + + s_electron = Species( + molecule=[m_electron], + thermo=ThermoData(Tdata=([300, 400, 500, 600, 800, 1000, 1500, 2000], + "K"), + Cpdata=([0., 0., 0., 0., 0., 0., 0., 0.], "cal/(mol*K)"), + H298=(0.0, "kcal/mol"), + S298=(0.0, "cal/(mol*K)"))) + + s_proton = Species( + molecule=[m_proton], + thermo = ThermoData( + Tdata=([300,400,500,600,800,1000,1500],'K'), + Cpdata=([3.4475,3.4875,3.497,3.5045,3.5405,3.6095,3.86],'cal/(mol*K)'), + H298=(0,'kcal/mol'), S298=(15.6165,'cal/(mol*K)','+|-',0.0007), + comment = '1/2 free energy of H2(g)')) + s_x = Species( + molecule=[m_x], + thermo=ThermoData(Tdata=([300, 400, 500, 600, 800, 1000, 1500, 2000], + "K"), + Cpdata=([0., 0., 0., 0., 0., 0., 0., 0.], "cal/(mol*K)"), + H298=(0.0, "kcal/mol"), + S298=(0.0, "cal/(mol*K)"))) + + s_ch2x = Species( + molecule=[m_ch2x], + thermo=ThermoData(Tdata=([300,400,500,600,800,1000,1500],'K'), + Cpdata=([28.4959,36.3588,42.0219,46.2428,52.3978,56.921,64.1119],'J/(mol*K)'), + H298=(0.654731,'kJ/mol'), S298=(19.8341,'J/(mol*K)'), Cp0=(0.01,'J/(mol*K)'), + CpInf=(99.7737,'J/(mol*K)'), + comment="""Thermo library: surfaceThermoPt111 Binding energy corrected by LSR (0.50C)""")) + + s_ch3x = Species( + molecule=[m_ch3x], + thermo=ThermoData(Tdata=([300,400,500,600,800,1000,1500],'K'), + Cpdata=([37.3325,44.9406,51.3613,56.8115,65.537,72.3287,83.3007],'J/(mol*K)'), + H298=(-45.8036,'kJ/mol'), S298=(57.7449,'J/(mol*K)'), Cp0=(0.01,'J/(mol*K)'), + CpInf=(124.717,'J/(mol*K)'), + comment="""Thermo library: surfaceThermoPt111 Binding energy corrected by LSR (0.25C)""")) + + # X=CH2 + H+ + e- <--> X-CH3 + rxn_reduction = Reaction(reactants=[s_proton, s_ch2x], + products=[s_ch3x], + electrons = -1, + kinetics=SurfaceChargeTransfer( + A = (2.483E21, 'cm^3/(mol*s)'), + V0 = (0, 'V'), + Ea = (10, 'kJ/mol'), + Tmin = (200, 'K'), + Tmax = (3000, 'K'), + electrons = -1, + )) + + rxn_oxidation = Reaction(products=[s_proton, s_ch2x], + reactants=[s_ch3x], + electrons = 1, + kinetics=SurfaceChargeTransfer( + A = (2.483E21, 'cm^3/(mol*s)'), + V0 = (0, 'V'), + Ea = (10, 'kJ/mol'), + Tmin = (200, 'K'), + Tmax = (3000, 'K'), + electrons = 1, + )) + + self.rxn_reduction = rxn_reduction + self.rxn_oxidation = rxn_oxidation + + def test_electrons(self): + """Test electrons property""" + self.assertEquals(self.rxn_reduction.electrons,-1) + self.assertEquals(self.rxn_oxidation.electrons,1) + + def test_protons(self): + """Test n_protons property""" + self.assertEquals(self.rxn_reduction.protons,-1) + self.assertEquals(self.rxn_oxidation.protons,1) + + def test_is_surface_reaction(self): + """Test is_surface_reaction() method""" + self.assertTrue(self.rxn_reduction.is_surface_reaction()) + self.assertTrue(self.rxn_oxidation.is_surface_reaction()) + + def test_is_charge_transfer_reaction(self): + """Test is_charge_transfer_reaction() method""" + self.assertTrue(self.rxn_reduction.is_charge_transfer_reaction()) + self.assertTrue(self.rxn_oxidation.is_charge_transfer_reaction()) + + def test_is_surface_charge_transfer(self): + """Test is_surface_charge_transfer() method""" + self.assertTrue(self.rxn_reduction.is_surface_charge_transfer_reaction()) + self.assertTrue(self.rxn_oxidation.is_surface_charge_transfer_reaction()) + + def test_get_reversible_potential(self): + """Test get_reversible_potential() method""" + V0_reduction = self.rxn_reduction.get_reversible_potential(298) + V0_oxidation = self.rxn_oxidation.get_reversible_potential(298) + + self.assertAlmostEqual(V0_reduction,V0_oxidation,6) + self.assertAlmostEqual(V0_reduction, 0.3967918, 6) + self.assertAlmostEqual(V0_oxidation, 0.3967918, 6) + + def test_get_rate_coeff(self): + """Test get_rate_coefficient() method""" + + # these should be the same + kf_1 = self.rxn_reduction.get_rate_coefficient(298,potential=0) + kf_2 = self.rxn_reduction.kinetics.get_rate_coefficient(298,0) + + self.assertAlmostEqual(kf_1, 43870506959779.0, 6) + self.assertAlmostEqual(kf_1, kf_2, 6) + + #kf_2 should be greater than kf_1 + kf_1 = self.rxn_oxidation.get_rate_coefficient(298,potential=0) + kf_2 = self.rxn_oxidation.get_rate_coefficient(298,potential=0.1) + self.assertGreater(kf_2,kf_1) + + def test_equilibrium_constant_surface_charge_transfer_kc(self): + """ + Test the equilibrium constants of type Kc of a surface charge transfer reaction. + """ + Tlist = numpy.arange(400.0, 1600.0, 200.0, numpy.float64) + Kclist0 = [1.39365463e+03, 1.78420988e+01, 2.10543835e+00, 6.07529099e-01, + 2.74458007e-01, 1.59985450e-01] #reduction + Kclist_reduction = self.rxn_reduction.get_equilibrium_constants(Tlist, type='Kc') + Kclist_oxidation = self.rxn_oxidation.get_equilibrium_constants(Tlist, type='Kc') + # Test a range of temperatures + for i in range(len(Tlist)): + self.assertAlmostEqual(Kclist_reduction[i] / Kclist0[i], 1.0, 6) + self.assertAlmostEqual(1 / Kclist_oxidation[i] / Kclist0[i], 1.0, 6) + + V0 = self.rxn_oxidation.get_reversible_potential(298) + Kc_reduction_equil = self.rxn_reduction.get_equilibrium_constant(298, V0) + Kc_oxidation_equil = self.rxn_oxidation.get_equilibrium_constant(298, V0) + self.assertAlmostEqual(Kc_oxidation_equil * Kc_reduction_equil, 1.0, 4) + C0 = 1e5 / constants.R / 298 + self.assertAlmostEqual(Kc_oxidation_equil, C0, 4) + self.assertAlmostEqual(Kc_reduction_equil, 1/C0, 4) + + @pytest.skip("Work in progress") + def test_reverse_surface_charge_transfer_rate(self): + """ + Test the reverse_surface_charge_transfer_rate() method + """ + kf_reduction = self.rxn_reduction.kinetics + kf_oxidation = self.rxn_oxidation.kinetics + kr_oxidation = self.rxn_oxidation.generate_reverse_rate_coefficient() + kr_reduction = self.rxn_reduction.generate_reverse_rate_coefficient() + self.assertEquals(kr_reduction.A.units, 's^-1') + self.assertEquals(kr_oxidation.A.units, 'm^3/(mol*s)') + + Tlist = numpy.linspace(298., 500., 30.,numpy.float64) + for T in Tlist: + for V in (-0.25,0.,0.25): + kf = kf_reduction.get_rate_coefficient(T,V) + kr = kr_reduction.get_rate_coefficient(T,V) + K = self.rxn_reduction.get_equilibrium_constant(T,V) + self.assertEquals(order_of_magnitude(kf/kr),order_of_magnitude(K)) + kf = kf_oxidation.get_rate_coefficient(T,V) + kr = kr_oxidation.get_rate_coefficient(T,V) + K = self.rxn_oxidation.get_equilibrium_constant(T,V) + self.assertEquals(order_of_magnitude(kf/kr),order_of_magnitude(K)) From 6b1585dfa72bc3cd8961bf7a74c27f5debece5a0 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 16:30:46 -0400 Subject: [PATCH 021/109] added family test for `Surface_Proton_Electron_Reduction_Alpha` family --- .../groups.py | 211 +++++++ .../rules.py | 26 + .../training/dictionary.txt | 64 +++ .../training/reactions.py | 516 ++++++++++++++++++ 4 files changed, 817 insertions(+) create mode 100644 rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py create mode 100644 rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py create mode 100644 rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt create mode 100644 rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py diff --git a/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py b/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py new file mode 100644 index 0000000000..656846b7f4 --- /dev/null +++ b/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# encoding: utf-8 + +name = "Surface_Proton_Electron_Reduction_Alpha/groups" +shortDesc = u"" +longDesc = u""" + + *1 *1-*3H + || + *3H+ + *e- ----> | + ~*2~ ~*2~~ + +The rate, which should be in mol/m2/s, +will be given by k * (mol/m2) * (mol/m3) * 1 +so k should be in (m3/mol/s). +""" + +template(reactants=["Adsorbate", "Proton"], products=["Reduced"], ownReverse=False) + +reverse = "Surface_Proton_Electron_Oxidation_Alpha" + +reactantNum = 2 +productNum = 1 +allowChargedSpecies = True +electrons = -1 + +recipe(actions=[ + ['LOSE_CHARGE', '*3', 1], + ['CHANGE_BOND', '*1', -1, '*2'], + ['FORM_BOND', '*1', 1, '*3'], +]) + +entry( + index = 1, + label = "Adsorbate", + group = +""" +1 *1 R!H u0 {2,[D,T,Q]} +2 *2 X u0 {1,[D,T,Q]} +""", + kinetics = None, +) + +entry( + index = 2, + label = "Proton", + group = +""" +1 *3 H+ u0 p0 c+1 +""", + kinetics = None, +) + +entry( + index = 4, + label = "CX", + group = +""" +1 *1 C u0 {2,[D,T,Q]} +2 *2 X u0 {1,[D,T,Q]} +""", + kinetics = None, +) + +entry( + index = 5, + label = "CTX", + group = +""" +1 *1 C u0 {2,T} +2 *2 X u0 {1,T} +""", + kinetics = None, +) + +entry( + index = 6, + label = "HCX", + group = +""" +1 *1 C u0 {2,T} {3,S} +2 *2 X u0 {1,T} +3 H u0 {1,S} +""", + kinetics = None, +) + +entry( + index = 7, + label = "C=X", + group = +""" +1 *1 C u0 {2,D} +2 *2 X u0 {1,D} +""", + kinetics = None, +) + +entry( + index = 8, + label = "H2C=X", + group = +""" +1 *1 C u0 {2,D} {3,S} {4,S} +2 *2 X u0 {1,D} +3 H u0 {1,S} +4 H u0 {1,S} +""", + kinetics = None, +) + +entry( + index = 9, + label = "O=C=X", + group = +""" +1 *1 C u0 {2,D} {3,D} +2 *2 X u0 {1,D} +3 O2d u0 {1,D} +""", + kinetics = None, +) + + +entry( + index = 10, + label = "OX", + group = +""" +1 *1 O u0 {2,D} +2 *2 X u0 {1,D} +""", + kinetics = None, +) + + +entry( + index = 11, + label = "NX", + group = +""" +1 *1 N u0 {2,[D,T]} +2 *2 X u0 {1,[D,T]} +""", + kinetics = None, +) + +entry( + index = 12, + label = "NTX", + group = +""" +1 *1 N u0 {2,T} +2 *2 X u0 {1,T} +""", + kinetics = None, +) + +entry( + index = 13, + label = "N=X", + group = +""" +1 *1 N u0 {2,D} +2 *2 X u0 {1,D} +""", + kinetics = None, +) + +entry( + index = 14, + label = "HN=X", + group = +""" +1 *1 N u0 {2,D} {3,S} +2 *2 X u0 {1,D} +3 N u0 {1,S} +""", + kinetics = None, +) + +entry( + index = 15, + label = "N-N=X", + group = +""" +1 *1 N u0 {2,D} {3,S} +2 *2 X u0 {1,D} +3 N u0 {1,S} +""", + kinetics = None, +) + +tree( +""" +L1: Adsorbate + L2: CX + L3: CTX + L4: HCX + L3: C=X + L4: O=C=X + L4: H2C=X + L2: OX + L2: NX + L3: NTX + L3: N=X + L4: HN=X + L4: N-N=X + +L1: Proton +""" +) diff --git a/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py b/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py new file mode 100644 index 0000000000..67a06429f2 --- /dev/null +++ b/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# encoding: utf-8 + +name = "Surface_Proton_Electron_Reduction_Alpha/rules" +shortDesc = u"" +longDesc = u""" +Surface adsorption of a single radical forming a single bond to the surface site +""" + +# entry( +# index = 1, +# label = "Adsorbate;Proton;Electron", +# kinetics = SurfaceChargeTransfer( +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff, 0 default +# V0 = None, # Reference potential +# Ea = (15, 'kJ/mol'), # activation energy at the reversible potential +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# rank = 0, +# shortDesc = u"""Default""", +# longDesc = u"""https://doi.org/10.1021/jp4100608""" +# ) + diff --git a/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt b/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt new file mode 100644 index 0000000000..c4d8a3f3e8 --- /dev/null +++ b/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt @@ -0,0 +1,64 @@ +H +1 *3 H u0 p0 c+1 + +NX +1 *1 N u0 p1 c0 {2,T} +2 *2 X u0 p0 c0 {1,T} + +HNX +1 *1 N u0 p1 c0 {2,D} {3,S} +2 *2 X u0 p0 c0 {1,D} +3 H u0 p0 c0 {1,S} + +HNX_p +1 *1 N u0 p1 c0 {2,D} {3,S} +2 *2 X u0 p0 c0 {1,D} +3 *3 H u0 p0 c0 {1,S} + +H2NX +1 *1 N u0 p1 c0 {2,S} {3,S} {4,S} +2 *2 X u0 p0 c0 {1,S} +3 *3 H u0 p0 c0 {1,S} +4 H u0 p0 c0 {1,S} + +CX +1 *1 C u0 p0 c0 {2,Q} +2 *2 X u0 p0 c0 {1,Q} + +CHX +1 *1 C u0 p0 c0 {2,T} {3,S} +2 *2 X u0 p0 c0 {1,T} +3 H u0 p0 c0 {1,S} + +CHX_p +1 *1 C u0 p0 c0 {2,T} {3,S} +2 *2 X u0 p0 c0 {1,T} +3 *3 H u0 p0 c0 {1,S} + +CH2X +1 *1 C u0 p0 c0 {2,D} {3,S} {4,S} +2 *2 X u0 p0 c0 {1,D} +3 H u0 p0 c0 {1,S} +4 H u0 p0 c0 {1,S} + +CH2X_p +1 *1 C u0 p0 c0 {2,D} {3,S} {4,S} +2 *2 X u0 p0 c0 {1,D} +3 *3 H u0 p0 c0 {1,S} +4 H u0 p0 c0 {1,S} + +CH3X +1 *1 C u0 p0 c0 {2,S} {3,S} {4,S} {5,S} +2 *2 X u0 p0 c0 {1,S} +3 *3 H u0 p0 c0 {1,S} +4 H u0 p0 c0 {1,S} +5 H u0 p0 c0 {1,S} + +OX +1 *1 O u0 p2 c0 {2,D} +2 *2 X u0 p0 c0 {1,D} + +HOX +1 *1 O u0 p2 c0 {2,S} {3,S} +2 *2 X u0 p0 c0 {1,S} +3 *3 H u0 p0 c0 {1,S} \ No newline at end of file diff --git a/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py b/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py new file mode 100644 index 0000000000..3866fa74ac --- /dev/null +++ b/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python +# encoding: utf-8 + +name = "Surface_Proton_Electron_Reduction_Alpha/training" +shortDesc = u"Reaction kinetics used to generate rate rules" +longDesc = u""" +Put kinetic parameters for specific reactions in this file to use as a +training set for generating rate rules to populate this kinetics family. +""" + +# entry( +# index = 1, +# label = "CX + H <=> CHX_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.20, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.24, 'V'), # reference potential +# Ea = (0.61, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Tafel""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +entry( + index = 2, + label = "CX + H <=> CHX_p", + degeneracy = 1, + kinetics = SurfaceChargeTransfer( + alpha = 0.20, # charge transfer coeff + A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ + n = 0, # temperature coeff + V0 = (-0.5, 'V'), # reference potential + Ea = (0.44, 'eV/molecule'), # activation energy + Tmin = (200, 'K'), + Tmax = (3000, 'K'), + electrons = -1, # electron stochiometric coeff + ), + shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", + longDesc = u"""Tafel""", + metal = "Pt", + facet = "111", + site = "", + rank = 5, +) + +# entry( +# index = 1, +# label = "CX + H <=> CHX_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.06, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.29, 'V'), # reference potential +# Ea = (0.19, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 1, +# label = "CX + H <=> CHX_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.06, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (-0.5, 'V'), # reference potential +# Ea = (0.13, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 3, +# label = "CHX + H <=> CH2X_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.31, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.32, 'V'), # reference potential +# Ea = (0.77, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Tafel""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +entry( + index = 3, + label = "CHX + H <=> CH2X_p", + degeneracy = 1, + kinetics = SurfaceChargeTransfer( + alpha = 0.31, # charge transfer coeff + A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ + n = 0, # temperature coeff + V0 = (-0.5, 'V'), # reference potential + Ea = (0.44, 'eV/molecule'), # activation energy + Tmin = (200, 'K'), + Tmax = (3000, 'K'), + electrons = -1, # electron stochiometric coeff + ), + shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", + longDesc = u"""Tafel""", + metal = "Pt", + facet = "111", + site = "", + rank = 5, +) + +# entry( +# index = 3, +# label = "CHX + H <=> CH2X_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.05, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.30, 'V'), # reference potential +# Ea = (0.59, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 4, +# label = "CHX + H <=> CH2X_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.05, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (-0.5, 'V'), # reference potential +# Ea = (0.53, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 6, +# label = "CH2X + H <=> CH3X", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.23, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.38, 'V'), # reference potential +# Ea = (0.62, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Tafel""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +entry( + index = 4, + label = "CH2X + H <=> CH3X", + degeneracy = 1, + kinetics = SurfaceChargeTransfer( + alpha = 0.23, # charge transfer coeff + A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ + n = 0, # temperature coeff + V0 = (-0.5, 'V'), # reference potential + Ea = (0.37, 'eV/molecule'), # activation energy + Tmin = (200, 'K'), + Tmax = (3000, 'K'), + electrons = -1, # electron stochiometric coeff + ), + shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", + longDesc = u"""Tafel""", + metal = "Pt", + facet = "111", + site = "", + rank = 5, +) + +# entry( +# index = 8, +# label = "NX + H <=> HNX", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.07, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (-0.12, 'V'), # reference potential +# Ea = (0.15, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2017.01.050""", +# longDesc = u""" +# """, +# metal = "Cu", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 9, +# label = "NX + H <=> HNX_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.23, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.24, 'V'), # reference potential +# Ea = (0.78, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Tafel""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +entry( + index = 5, + label = "NX + H <=> HNX_p", + degeneracy = 1, + kinetics = SurfaceChargeTransfer( + alpha = 0.23, # charge transfer coeff + A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ + n = 0, # temperature coeff + V0 = (-0.5, 'V'), # reference potential + Ea = (0.59, 'eV/molecule'), # activation energy + Tmin = (200, 'K'), + Tmax = (3000, 'K'), + electrons = -1, # electron stochiometric coeff + ), + shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", + longDesc = u"""Tafel""", + metal = "Pt", + facet = "111", + site = "", + rank = 5, +) + +# entry( +# index = 10, +# label = "NX + H <=> HNX_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.07, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.3, 'V'), # reference potential +# Ea = (0.17, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 4, +# label = "NX + H <=> HNX_p", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.07, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (-0.5, 'V'), # reference potential +# Ea = (0.09, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 11, +# label = "HNX + H <=> H2NX", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.27, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.28, 'V'), # reference potential +# Ea = (1.20, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Tafel""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +entry( + index = 6, + label = "HNX + H <=> H2NX", + degeneracy = 1, + kinetics = SurfaceChargeTransfer( + alpha = 0.27, # charge transfer coeff + A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ + n = 0, # temperature coeff + V0 = (-0.5, 'V'), # reference potential + Ea = (0.97, 'eV/molecule'), # activation energy + Tmin = (200, 'K'), + Tmax = (3000, 'K'), + electrons = -1, # electron stochiometric coeff + ), + shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", + longDesc = u"""Tafel""", + metal = "Pt", + facet = "111", + site = "", + rank = 5, +) + +# entry( +# index = 11, +# label = "HNX + H <=> H2NX", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.64, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.31, 'V'), # reference potential +# Ea = (0.99, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 5, +# label = "HNX + H <=> H2NX", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.64, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (-0.5, 'V'), # reference potential +# Ea = (0.43, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 12, +# label = "OX + H <=> HOX", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.41, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.31, 'V'), # reference potential +# Ea = (0.87, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Tafel""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +entry( + index = 7, + label = "OX + H <=> HOX", + degeneracy = 1, + kinetics = SurfaceChargeTransfer( + alpha = 0.42, # charge transfer coeff + A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ + n = 0, # temperature coeff + V0 = (-0.5, 'V'), # reference potential + Ea = (0.48, 'eV/molecule'), # activation energy + Tmin = (200, 'K'), + Tmax = (3000, 'K'), + electrons = -1, # electron stochiometric coeff + ), + shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", + longDesc = u"""Tafel""", + metal = "Pt", + facet = "111", + site = "", + rank = 5, +) + +# entry( +# index = 12, +# label = "OX + H <=> HOX", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.02, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (0.57, 'V'), # reference potential +# Ea = (0.06, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) + +# entry( +# index = 6, +# label = "OX + H <=> HOX", +# degeneracy = 1, +# kinetics = SurfaceChargeTransfer( +# alpha = 0.02, # charge transfer coeff +# A = (2.5e14, 'm^3/(mol*s)'), # pre-exponential factor estimate 10^11 s^-1 * 2.5e5 m^2/mol / 1000 m^3/mol H+ +# n = 0, # temperature coeff +# V0 = (-0.5, 'V'), # reference potential +# Ea = (0.03, 'eV/molecule'), # activation energy +# Tmin = (200, 'K'), +# Tmax = (3000, 'K'), +# electrons = -1, # electron stochiometric coeff +# ), +# shortDesc = u"""https://doi.org/10.1016/j.cattod.2018.03.048""", +# longDesc = u"""Heyrovsky""", +# metal = "Pt", +# facet = "111", +# site = "", +# rank = 5, +# ) From 21a09c2da8f88261ec943d4690b8b1062ad70b2d Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 16:33:47 -0400 Subject: [PATCH 022/109] update charge before updating lone pairs for solvation `transform_lone_pairs` method --- rmgpy/data/solvation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index 45a133de01..0e4f7d6a55 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -1311,6 +1311,7 @@ def estimate_radical_solute_data_via_hbi(self, molecule, stable_solute_data_esti saturated_struct.remove_bond(bond) saturated_struct.remove_atom(H) atom.increment_radical() + saturated_struct.update_charge() # we need to update charges before updating lone pairs saturated_struct.update() try: self._add_group_solute_data(solute_data, self.groups['radical'], saturated_struct, {'*': atom}) From 42664623686b31dcb397dbdf9ff4ca13848157c1 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Wed, 12 May 2021 16:40:36 -0400 Subject: [PATCH 023/109] update_atomtypes -> update in translator.py --- rmgpy/molecule/translator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rmgpy/molecule/translator.py b/rmgpy/molecule/translator.py index a19746b76f..fece53b041 100644 --- a/rmgpy/molecule/translator.py +++ b/rmgpy/molecule/translator.py @@ -519,7 +519,7 @@ def _read(mol, identifier, identifier_type, backend, raise_atomtype_exception=Tr if _lookup(mol, identifier, identifier_type) is not None: if _check_output(mol, identifier): - mol.update_atomtypes(log_species=True, raise_exception=raise_atomtype_exception) + mol.update(log_species=True, raise_atomtype_exception=raise_atomtype_exception, sort_atoms=False) return mol for option in _get_backend_list(backend): @@ -531,7 +531,7 @@ def _read(mol, identifier, identifier_type, backend, raise_atomtype_exception=Tr raise NotImplementedError("Unrecognized backend {0}".format(option)) if _check_output(mol, identifier): - mol.update_atomtypes(log_species=True, raise_exception=raise_atomtype_exception) + mol.update(log_species=True, raise_atomtype_exception=raise_atomtype_exception, sort_atoms=False) return mol else: logging.debug('Backend {0} is not able to parse identifier {1}'.format(option, identifier)) From 4b934ce39f114e127c70347740dd0744658c533c Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Thu, 2 Sep 2021 14:53:26 -0400 Subject: [PATCH 024/109] added `adsorption_groups` attr to thermoDB --- rmgpy/data/rmg.py | 6 ++++-- rmgpy/data/thermo.py | 9 +++++++-- rmgpy/rmg/input.py | 2 ++ rmgpy/rmg/main.py | 6 +++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/rmgpy/data/rmg.py b/rmgpy/data/rmg.py index 9dd230c51a..1bf8ba4eea 100644 --- a/rmgpy/data/rmg.py +++ b/rmgpy/data/rmg.py @@ -80,6 +80,7 @@ def load(self, kinetics_families=None, kinetics_depositories=None, statmech_libraries=None, + adsorption_groups='adsorptionPt111', depository=True, solvation=True, surface=True, # on by default, because solvation is also on by default @@ -109,16 +110,17 @@ def load(self, self.load_solvation(os.path.join(path, 'solvation')) if surface: - self.load_thermo(os.path.join(path, 'thermo'), thermo_libraries, depository, surface) + self.load_thermo(os.path.join(path, 'thermo'), thermo_libraries, depository, surface, adsorption_groups) - def load_thermo(self, path, thermo_libraries=None, depository=True, surface=False): + def load_thermo(self, path, thermo_libraries=None, depository=True, surface=False, adsorption_groups='adsorptionPt111'): """ Load the RMG thermo database from the given `path` on disk, where `path` points to the top-level folder of the RMG thermo database. """ self.thermo = ThermoDatabase() + self.thermo.adsorption_groups = adsorption_groups self.thermo.load(path, thermo_libraries, depository, surface) def load_transport(self, path, transport_libraries=None): diff --git a/rmgpy/data/thermo.py b/rmgpy/data/thermo.py index 7816d7ed2f..a971565ad8 100644 --- a/rmgpy/data/thermo.py +++ b/rmgpy/data/thermo.py @@ -854,6 +854,7 @@ def __init__(self): self.libraries = {} self.surface = {} self.groups = {} + self.adsorption_groups = "adsorptionPt111" self.library_order = [] self.local_context = { 'ThermoData': ThermoData, @@ -989,7 +990,9 @@ def load_groups(self, path): 'longDistanceInteraction_cyclic', 'longDistanceInteraction_noncyclic', 'adsorptionPt111', + 'adsorptionLi' ] + # categories.append(self.adsorption_groups) self.groups = { category: ThermoGroups(label=category).load(os.path.join(path, category + '.py'), self.local_context, self.global_context) @@ -1287,7 +1290,9 @@ def get_thermo_data(self, species, metal_to_scale_to=None, training_set=None): if species.contains_surface_site(): try: thermo0 = self.get_thermo_data_for_surface_species(species) - thermo0 = self.correct_binding_energy(thermo0, species, metal_to_scale_from="Pt111", metal_to_scale_to=metal_to_scale_to) # group adsorption values come from Pt111 + metal_to_scale_from = self.adsorption_groups.split('adsorption')[-1] + if metal_to_scale_from != metal_to_scale_to: + thermo0 = self.correct_binding_energy(thermo0, species, metal_to_scale_from=metal_to_scale_from, metal_to_scale_to=metal_to_scale_to) # group adsorption values come from Pt111 return thermo0 except: logging.error("Error attempting to get thermo for species %s with structure \n%s", @@ -1607,7 +1612,7 @@ def lowest_energy(species): surface_sites = molecule.get_surface_sites() try: - self._add_adsorption_correction(adsorption_thermo, self.groups['adsorptionPt111'], molecule, surface_sites) + self._add_adsorption_correction(adsorption_thermo, self.groups[self.adsorption_groups], molecule, surface_sites) except (KeyError, DatabaseError): logging.error("Couldn't find in adsorption thermo database:") logging.error(molecule) diff --git a/rmgpy/rmg/input.py b/rmgpy/rmg/input.py index e04cd82e2e..c94a7f58e1 100644 --- a/rmgpy/rmg/input.py +++ b/rmgpy/rmg/input.py @@ -67,6 +67,7 @@ def database( kineticsFamilies='default', kineticsDepositories='default', kineticsEstimator='rate rules', + adsorptionGroups='adsorptionPt111' ): # This function just stores the information about the database to be loaded # We don't actually load the database until after we're finished reading @@ -101,6 +102,7 @@ def database( "['H_Abstraction','R_Recombination'] or ['!Intra_Disproportionation'].") rmg.kinetics_families = kineticsFamilies + rmg.adsorption_groups = adsorptionGroups def catalyst_properties(bindingEnergies=None, surfaceSiteDensity=None, diff --git a/rmgpy/rmg/main.py b/rmgpy/rmg/main.py index 8df771980d..873991baef 100644 --- a/rmgpy/rmg/main.py +++ b/rmgpy/rmg/main.py @@ -406,7 +406,9 @@ def load_database(self): seed_mechanisms=self.seed_mechanisms, kinetics_families=self.kinetics_families, kinetics_depositories=self.kinetics_depositories, - statmech_libraries=self.statmech_libraries, + statmech_libraries = self.statmech_libraries, + adsorption_groups='adsorptionPt111', # use Pt111 groups for training reactions + # frequenciesLibraries = self.statmech_libraries, depository=False, # Don't bother loading the depository information, as we don't use it ) @@ -490,6 +492,8 @@ def load_database(self): if not family.auto_generated: family.fill_rules_by_averaging_up(verbose=self.verbose_comments) + self.database.thermo.adsorption_groups = self.adsorption_groups + def initialize(self, **kwargs): """ Initialize an RMG job using the command-line arguments `args` as returned From bfd9f5a6401f4a1c9cd6e2f0ea3779fd3d1cbc73 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Thu, 2 Sep 2021 14:54:58 -0400 Subject: [PATCH 025/109] added fluorine to thermo `correct_binding_energy` method --- rmgpy/data/thermo.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/rmgpy/data/thermo.py b/rmgpy/data/thermo.py index a971565ad8..4bedb515d7 100644 --- a/rmgpy/data/thermo.py +++ b/rmgpy/data/thermo.py @@ -1486,10 +1486,14 @@ def correct_binding_energy(self, thermo, species, metal_to_scale_from=None, meta 'H': rmgpy.quantity.Energy(0.0, 'eV/molecule'), 'O': rmgpy.quantity.Energy(0.0, 'eV/molecule'), 'N': rmgpy.quantity.Energy(0.0, 'eV/molecule'), + 'F': rmgpy.quantity.Energy(0.0, 'eV/molecule'), } for element, delta_energy in delta_atomic_adsorption_energy.items(): - delta_energy.value_si = metal_to_scale_to_binding_energies[element].value_si - metal_to_scale_from_binding_energies[element].value_si + try: + delta_energy.value_si = metal_to_scale_to_binding_energies[element].value_si - metal_to_scale_from_binding_energies[element].value_si + except KeyError: + pass if all(-0.01 < v.value_si < 0.01 for v in delta_atomic_adsorption_energy.values()): return thermo @@ -1500,8 +1504,8 @@ def correct_binding_energy(self, thermo, species, metal_to_scale_from=None, meta for atom in molecule.atoms: if atom.is_surface_site(): surface_sites.append(atom) - normalized_bonds = {'C': 0., 'O': 0., 'N': 0., 'H': 0.} - max_bond_order = {'C': 4., 'O': 2., 'N': 3., 'H': 1.} + normalized_bonds = {'C': 0., 'O': 0., 'N': 0., 'H': 0., 'F': 0.} + max_bond_order = {'C': 4., 'O': 2., 'N': 3., 'H': 1., 'F': 1} for site in surface_sites: numbonds = len(site.bonds) if numbonds == 0: @@ -1531,11 +1535,14 @@ def correct_binding_energy(self, thermo, species, metal_to_scale_from=None, meta # now edit the adsorptionThermo using LSR comments = [] - for element in 'CHON': - if normalized_bonds[element]: - change_in_binding_energy = delta_atomic_adsorption_energy[element].value_si * normalized_bonds[element] + for element,bond in normalized_bonds.items(): + if bond: + try: + change_in_binding_energy = delta_atomic_adsorption_energy[element].value_si * bond + except KeyError: + continue thermo.H298.value_si += change_in_binding_energy - comments.append(f'{normalized_bonds[element]:.2f}{element}') + comments.append(f'{bond:.2f}{element}') thermo.comment += " Binding energy corrected by LSR ({}) from {}".format('+'.join(comments), metal_to_scale_from) return thermo From 341601eff68e719d9d52d4b7a0d16784e668cfb3 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Mon, 4 Oct 2021 17:49:23 -0400 Subject: [PATCH 026/109] added Li/Li0/Li+ atomtype --- arkane/encorr/bac.py | 2 +- rmgpy/data/solvation.py | 18 +++++++++--------- rmgpy/data/thermo.py | 4 ++-- rmgpy/molecule/adjlist.py | 2 +- rmgpy/molecule/atomtype.py | 16 +++++++++++++++- rmgpy/molecule/element.py | 20 ++++++++++++++------ rmgpy/molecule/fragment.py | 3 +++ rmgpy/molecule/group.py | 8 ++++++++ rmgpy/molecule/molecule.py | 9 ++++++++- 9 files changed, 61 insertions(+), 21 deletions(-) diff --git a/arkane/encorr/bac.py b/arkane/encorr/bac.py index 564cadeb15..c511a9f47b 100644 --- a/arkane/encorr/bac.py +++ b/arkane/encorr/bac.py @@ -241,7 +241,7 @@ class BAC: ref_databases = {} atom_spins = { 'H': 0.5, 'C': 1.0, 'N': 1.5, 'O': 1.0, 'F': 0.5, - 'Si': 1.0, 'P': 1.5, 'S': 1.0, 'Cl': 0.5, 'Br': 0.5, 'I': 0.5 + 'Si': 1.0, 'P': 1.5, 'S': 1.0, 'Cl': 0.5, 'Br': 0.5, 'I': 0.5, 'Li': 0.5, } exp_coeff = 3.0 # Melius-type parameter (Angstrom^-1) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index 0e4f7d6a55..892a81e53b 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -470,7 +470,7 @@ class SoluteData(object): """ # Set class variable with McGowan volumes mcgowan_volumes = { - 1: 8.71, 2: 6.75, + 1: 8.71, 2: 6.75, 3: 22.23, 6: 16.35, 7: 14.39, 8: 12.43, 9: 10.47, 10: 8.51, 14: 26.83, 15: 24.87, 16: 22.91, 17: 20.95, 18: 18.99, 35: 26.21, 53: 34.53, @@ -491,7 +491,7 @@ def __repr__(self): def get_stokes_diffusivity(self, T, solvent_viscosity): """ - Get diffusivity of solute using the Stokes-Einstein sphere relation. + Get diffusivity of solute using the Stokes-Einstein sphere relation. Radius is found from the McGowan volume. solvent_viscosity should be given in kg/s/m which equals Pa.s (water is about 9e-4 Pa.s at 25C, propanol is 2e-3 Pa.s) @@ -510,7 +510,7 @@ def set_mcgowan_volume(self, species): doi: 10.1007/BF02311772 Also see Table 1 in Zhao et al., J. Chem. Inf. Comput. Sci. Vol. 43, p.1848. 2003 doi: 10.1021/ci0341114 - + "V is scaled to have similar values to the other descriptors by division by 100 and has units of (cm3mol−1/100)." the contibutions in this function are in cm3/mol, and the division by 100 is done at the very end. @@ -859,7 +859,7 @@ def load(self, path, libraries=None, depository=True): """ Load the solvation database from the given `path` on disk, where `path` points to the top-level folder of the solvation database. - + Load the solvent and solute libraries, then the solute groups. """ @@ -1154,8 +1154,8 @@ def get_all_solute_data(self, species): Return all possible sets of Abraham solute descriptors for a given :class:`Species` object `species`. The hits from the library come first, then the group additivity estimate. This method is useful - for a generic search job. Right now, there should either be 1 or - 2 sets of descriptors, depending on whether or not we have a + for a generic search job. Right now, there should either be 1 or + 2 sets of descriptors, depending on whether or not we have a library entry. """ solute_data_list = [] @@ -1192,7 +1192,7 @@ def get_solute_data_from_groups(self, species): :class:`Species` object `species` by estimation using the group additivity method. If no group additivity values are loaded, a :class:`DatabaseError` is raised. - + It estimates the solute data for the first item in the species's molecule list because it is the most stable resonance structure found by gas-phase thermo estimate. @@ -1922,7 +1922,7 @@ def calc_s(self, delG, delH): return delS def get_solvation_correction(self, solute_data, solvent_data): - """ + """ Given a solute_data and solvent_data object, calculates the enthalpy, entropy, and Gibbs free energy of solvation at 298 K. Returns a SolvationCorrection object @@ -2133,7 +2133,7 @@ def get_Kfactor_parameters(self, delG298, delH298, delS298, solvent_name, T_tran kfactor_parameters.T_transition = T_transition return kfactor_parameters - + def check_solvent_in_initial_species(self, rmg, solvent_structure): """ Given the instance of RMG class and the solvent_structure, it checks whether the solvent is listed as one diff --git a/rmgpy/data/thermo.py b/rmgpy/data/thermo.py index 4bedb515d7..26d2b20218 100644 --- a/rmgpy/data/thermo.py +++ b/rmgpy/data/thermo.py @@ -1504,8 +1504,8 @@ def correct_binding_energy(self, thermo, species, metal_to_scale_from=None, meta for atom in molecule.atoms: if atom.is_surface_site(): surface_sites.append(atom) - normalized_bonds = {'C': 0., 'O': 0., 'N': 0., 'H': 0., 'F': 0.} - max_bond_order = {'C': 4., 'O': 2., 'N': 3., 'H': 1., 'F': 1} + normalized_bonds = {'C': 0., 'O': 0., 'N': 0., 'H': 0., 'F': 0., 'Li': 0.} + max_bond_order = {'C': 4., 'O': 2., 'N': 3., 'H': 1., 'F': 1, 'Li': 1.} for site in surface_sites: numbonds = len(site.bonds) if numbonds == 0: diff --git a/rmgpy/molecule/adjlist.py b/rmgpy/molecule/adjlist.py index c75b13086b..852d7bc9d0 100644 --- a/rmgpy/molecule/adjlist.py +++ b/rmgpy/molecule/adjlist.py @@ -92,7 +92,7 @@ def check_partial_charge(atom): the theoretical one: """ - if atom.symbol in {'X','L','R','e','H+'}: + if atom.symbol in {'X','L','R','e','H+','Li'}: return # because we can't check it. valence = PeriodicSystem.valence_electrons[atom.symbol] diff --git a/rmgpy/molecule/atomtype.py b/rmgpy/molecule/atomtype.py index 0097de9f4b..960648c6ee 100644 --- a/rmgpy/molecule/atomtype.py +++ b/rmgpy/molecule/atomtype.py @@ -304,6 +304,7 @@ def get_features(self): # Non-surface atomTypes, R being the most generic: ATOMTYPES['R'] = AtomType(label='R', generic=['Rx'], specific=[ 'H','H0','H+', + 'Li','Li0','Li+', 'R!H', 'R!H!Val7', 'Val4','Val5','Val6','Val7', @@ -322,6 +323,7 @@ def get_features(self): ATOMTYPES['R!H'] = AtomType(label='R!H', generic=['R', 'Rx', 'Rx!H'], specific=[ 'Val4','Val5','Val6','Val7', 'He','Ne','Ar', + 'Li','Li0','Li+', 'C','Ca','Cs','Csc','Cd','CO','CS','Cdd','Cdc','Ctc','Ct','Cb','Cbf','Cq','C2s','C2sc','C2d','C2dc','C2tc', 'N','N0sc','N1s','N1sc','N1dc','N3s','N3sc','N3d','N3t','N3b','N5sc','N5dc','N5ddc','N5dddc','N5tc','N5b','N5bd', 'O','Oa','O0sc','O2s','O2sc','O2d','O4sc','O4dc','O4tc','O4b', @@ -336,6 +338,7 @@ def get_features(self): ATOMTYPES['R!H!Val7'] = AtomType(label='R!H!Val7', generic=['R', 'Rx', 'Rx!H'], specific=[ 'Val4','Val5','Val6', 'He','Ne','Ar', + 'Li','Li0','Li+', 'C','Ca','Cs','Csc','Cd','CO','CS','Cdd','Cdc','Ctc','Ct','Cb','Cbf','Cq','C2s','C2sc','C2d','C2dc','C2tc', 'N','N0sc','N1s','N1sc','N1dc','N3s','N3sc','N3d','N3t','N3b','N5sc','N5dc','N5ddc','N5dddc','N5tc','N5b','N5bd', 'O','Oa','O0sc','O2s','O2sc','O2d','O4sc','O4dc','O4tc','O4b', @@ -369,6 +372,13 @@ def get_features(self): ATOMTYPES['H+'] = AtomType('H+', generic=['R','H'], specific=[], single=[0], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[0], charge=[+1]) +ATOMTYPES['Li'] = AtomType('Li', generic=['R', 'R!H', 'R!H!Val7'], specific=['Li0','Li+'], + single=[0,1], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[0], charge=[0,1]) +ATOMTYPES['Li0'] = AtomType('Li', generic=['Li','R', 'R!H', 'R!H!Val7'], specific=[], + single=[0,1], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[0], charge=[0]) +ATOMTYPES['Li+'] = AtomType('Li+', generic=['Li','R', 'R!H', 'R!H!Val7'], specific=[], + single=[0], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[0], charge=[1]) + ATOMTYPES['He'] = AtomType('He', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[]) ATOMTYPES['Ne'] = AtomType('Ne', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[]) ATOMTYPES['Ar'] = AtomType('Ar', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[]) @@ -709,6 +719,10 @@ def get_features(self): ATOMTYPES['H0'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['H0'], break_bond=['H0'], increment_radical=['H0'], decrement_radical=['H0'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['H+'], decrement_charge=[]) ATOMTYPES['H+'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=['H0']) +ATOMTYPES['Li'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Li'], break_bond=['Li'], increment_radical=['Li'], decrement_radical=['Li'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['Li'], decrement_charge=['Li']) +ATOMTYPES['Li0'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Li0'], break_bond=['Li0'], increment_radical=['Li0'], decrement_radical=['H0'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['Li+'], decrement_charge=[]) +ATOMTYPES['Li+'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=['Li0']) + ATOMTYPES['He'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['He'], decrement_radical=['He'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) ATOMTYPES['Ar'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) @@ -834,7 +848,7 @@ def get_features(self): ATOMTYPES['F1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F1s'], break_bond=['F1s'], increment_radical=['F1s'], decrement_radical=['F1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) # these are ordered in priority of picking if a more general atomtype is encountered -allElements = ['H', 'C', 'O', 'N', 'S', 'P', 'Si', 'F', 'Cl', 'Br', 'I', 'Ne', 'Ar', 'He', 'X', 'e'] +allElements = ['H', 'C', 'O', 'N', 'S', 'P', 'Si', 'F', 'Cl', 'Br', 'I', 'Li', 'Ne', 'Ar', 'He', 'X', 'e', ] # list of elements that do not have more specific atomTypes nonSpecifics = ['He', 'Ne', 'Ar', 'e'] diff --git a/rmgpy/molecule/element.py b/rmgpy/molecule/element.py index 0f5abfe4f4..6ec975fcf9 100644 --- a/rmgpy/molecule/element.py +++ b/rmgpy/molecule/element.py @@ -123,13 +123,14 @@ class PeriodicSystem(object): isotopes of the same element may have slight different electronegativities, which is not reflected below """ valences = {'H+':0, 'e': 0, 'H': 1, 'He': 0, 'C': 4, 'N': 3, 'O': 2, 'F': 1, 'Ne': 0, - 'Si': 4, 'P': 3, 'S': 2, 'Cl': 1, 'Br': 1, 'Ar': 0, 'I': 1, 'X': 4} + 'Si': 4, 'P': 3, 'S': 2, 'Cl': 1, 'Br': 1, 'Ar': 0, 'I': 1, 'X': 4, 'Li': 1} valence_electrons = {'H+':0, 'e': 1, 'H': 1, 'He': 2, 'C': 4, 'N': 5, 'O': 6, 'F': 7, 'Ne': 8, - 'Si': 4, 'P': 5, 'S': 6, 'Cl': 7, 'Br': 7, 'Ar': 8, 'I': 7, 'X': 4} + 'Si': 4, 'P': 5, 'S': 6, 'Cl': 7, 'Br': 7, 'Ar': 8, 'I': 7, 'X': 4, 'Li': 1} lone_pairs = {'H+':0, 'e': 0, 'H': 0, 'He': 1, 'C': 0, 'N': 1, 'O': 2, 'F': 3, 'Ne': 4, - 'Si': 0, 'P': 1, 'S': 2, 'Cl': 3, 'Br': 3, 'Ar': 4, 'I': 3, 'X': 0} + 'Si': 0, 'P': 1, 'S': 2, 'Cl': 3, 'Br': 3, 'Ar': 4, 'I': 3, 'X': 0, 'Li': 0} electronegativity = {'H': 2.20, 'D': 2.20, 'T': 2.20, 'C': 2.55, 'C13': 2.55, 'N': 3.04, 'O': 3.44, 'O18': 3.44, - 'F': 3.98, 'Si': 1.90, 'P': 2.19, 'S': 2.58, 'Cl': 3.16, 'Br': 2.96, 'I': 2.66, 'X': 0.0} + 'F': 3.98, 'Si': 1.90, 'P': 2.19, 'S': 2.58, 'Cl': 3.16, 'Br': 2.96, 'I': 2.66, 'X': 0.0, + 'Li' : 0.98} ################################################################################ @@ -335,12 +336,14 @@ def get_element(value, isotope=-1): # P=P value is from: https://www2.chemistry.msu.edu/faculty/reusch/OrgPage/bndenrgy.htm # C#S is the value for [C+]#[S-] from 10.1002/chem.201002840 referenced relative to 0 K # X-O and X-X (X=F,Cl,Br) taken from https://labs.chem.ucsb.edu/zakarian/armen/11---bonddissociationenergy.pdf +# Li-C and Li-S are taken referenced from 0K from from http://staff.ustc.edu.cn/~luo971/2010-91-CRC-BDEs-Tables.pdf +# Li-N is a G3 calculation taken from https://doi.org/10.1021/jp050857o # The reference state is gaseous state at 298 K, but some of the values in the bde_dict might be coming from 0 K. # The bond dissociation energy at 298 K is greater than the bond dissociation energy at 0 K by 0.6 to 0.9 kcal/mol # (between RT and 3/2 RT), and this difference is usually much smaller than the uncertainty in the bond dissociation # energy itself. Therefore, the discrepancy between 0 K and 298 K shouldn't matter too much. # But for any new entries, try to use the consistent reference state of 298 K. -bde_elements = ['C', 'N', 'H', 'O', 'S', 'Cl', 'Si', 'P', 'F', 'Br', 'I'] # elements supported by BDE +bde_elements = ['C', 'N', 'H', 'O', 'S', 'Cl', 'Si', 'P', 'F', 'Br', 'I', 'Li'] # elements supported by BDE bde_dict = {('H', 'H', 1.0): (432.0, 'kJ/mol'), ('H', 'C', 1): (411.0, 'kJ/mol'), ('H', 'N', 1): (386.0, 'kJ/mol'), ('H', 'O', 1.0): (459.0, 'kJ/mol'), ('H', 'P', 1): (322.0, 'kJ/mol'), ('H', 'S', 1): (363.0, 'kJ/mol'), @@ -378,7 +381,12 @@ def get_element(value, isotope=-1): ('Br', 'Br', 1): (190.0, 'kJ/mol'), ('I', 'I', 1): (148.0, 'kJ/mol'), ('F', 'O', 1): (222.0, 'kJ/mol'), ('Cl', 'O', 1): (272.0, 'kJ/mol'), ('Br', 'O', 1): (235.1, 'kJ/mol'), ('Cl', 'F', 1): (250.54, 'kJ/mol'), - ('Br', 'F', 1): (233.8, 'kJ/mol'), ('Br', 'Cl', 1): (218.84, 'kJ/mol')} + ('Br', 'F', 1): (233.8, 'kJ/mol'), ('Br', 'Cl', 1): (218.84, 'kJ/mol'), + ('Li', 'Br', 1): (423.0, 'kJ/mol'), ('Li', 'F', 1): (577.0, 'kJ/mol'), + ('Li', 'Cl', 1): (469.0, 'kJ/mol'), ('Li', 'H', 1): (247.0, 'kJ/mol'), + ('Li', 'I', 1): (352.0, 'kJ/mol'), ('Li', 'O', 1): (341.6, 'kJ/mol'), + ('Li', 'C', 1): (214.6, 'kJ/mol'), ('Li', 'S', 1): (312.5, 'kJ/mol'), + ('Li', 'N', 1): (302.4, 'kJ/mol')} bdes = {} for key, value in bde_dict.items(): diff --git a/rmgpy/molecule/fragment.py b/rmgpy/molecule/fragment.py index 113628e8b1..8408c82b6f 100644 --- a/rmgpy/molecule/fragment.py +++ b/rmgpy/molecule/fragment.py @@ -165,6 +165,9 @@ def __str__(self): return "{0}({1:d})".format(self.label, self.index) # override methods + def is_lithium(self): + return False + def copy(self, deep=False): """ Create a copy of the current graph. If `deep` is ``True``, a deep copy diff --git a/rmgpy/molecule/group.py b/rmgpy/molecule/group.py index e12ac762ca..9cb2297636 100644 --- a/rmgpy/molecule/group.py +++ b/rmgpy/molecule/group.py @@ -670,6 +670,14 @@ def is_bromine(self): check_list = [x in all_bromine for x in self.atomtype] return all(check_list) + def is_lithium(self): + """ + Return ``True`` if the atom represents a bromine atom or ``False`` if not. + """ + all_lithium = [ATOMTYPES['Li']] + ATOMTYPES['Li'].specific + check_list = [x in all_lithium for x in self.atomtype] + return all(check_list) + def has_wildcards(self): """ Return ``True`` if the atom has wildcards in any of the attributes: diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index 0c10b51edf..26ae067998 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -399,6 +399,13 @@ def is_carbon(self): """ return self.element.number == 6 + def is_lithium(self): + """ + Return ``True`` if the atom represents a hydrogen atom or ``False`` if + not. + """ + return self.element.number == 3 + def is_nitrogen(self): """ Return ``True`` if the atom represents a nitrogen atom or ``False`` if @@ -2357,7 +2364,7 @@ def update_lone_pairs(self): """ cython.declare(atom1=Atom, atom2=Atom, bond12=Bond, order=float) for atom1 in self.vertices: - if atom1.is_hydrogen() or atom1.is_surface_site() or atom1.is_electron(): + if atom1.is_hydrogen() or atom1.is_surface_site() or atom1.is_electron() or atom1.is_lithium(): atom1.lone_pairs = 0 else: order = atom1.get_total_bond_order() From 0a2f429a6250dc663ccc6ffa3a6d688db866f03d Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Mon, 4 Oct 2021 17:43:31 -0400 Subject: [PATCH 027/109] add LiH and LiF to atom energy corrections fitting species list --- arkane/encorr/ae.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arkane/encorr/ae.py b/arkane/encorr/ae.py index b06bd025c9..779de3072b 100644 --- a/arkane/encorr/ae.py +++ b/arkane/encorr/ae.py @@ -73,7 +73,9 @@ 'Methane', 'Methyl', 'Ammonia', - 'Chloromethane' + 'Chloromethane', + 'Lithium Hydride', + 'Lithium Fluoride' ] From cc059ad8aacacc61c1723498f23a82a15de88f07 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Fri, 8 Oct 2021 09:21:53 -0400 Subject: [PATCH 028/109] handle SurfaceChargeTransfer in to_rms --- rmgpy/rmg/reactors.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index c487601f93..bbfab61df8 100644 --- a/rmgpy/rmg/reactors.py +++ b/rmgpy/rmg/reactors.py @@ -66,7 +66,7 @@ from rmgpy.kinetics.falloff import Troe, ThirdBody, Lindemann from rmgpy.kinetics.chebyshev import Chebyshev from rmgpy.data.solvation import SolventData -from rmgpy.kinetics.surface import StickingCoefficient +from rmgpy.kinetics.surface import StickingCoefficient, SurfaceChargeTransfer from rmgpy.solver.termination import TerminationTime, TerminationConversion, TerminationRateRatio from rmgpy.data.kinetics.family import TemplateReaction from rmgpy.data.kinetics.depository import DepositoryReaction @@ -590,6 +590,16 @@ def to_rms(obj, species_names=None, rms_species_list=None, rmg_species=None): n = obj._n.value_si Ea = obj._Ea.value_si return rms.Arrhenius(A, n, Ea, rms.EmptyRateUncertainty()) + elif isinstance(obj, SurfaceChargeTransfer): + A = obj._A.value_si + if obj._T0.value_si != 1.0: + A /= ((obj._T0.value_si) ** obj._n.value_si) + if obj._V0.value_si != 0.0: + A *= np.exp(obj._alpha.value_si*obj._electrons.value_si*constants.F*obj.V0.value_si) + n = obj._n.value_si + Ea = obj._Ea.value_si + q = obj._alpha.value_si*obj._electrons.value_si + return rms.Arrheniusq(A, n, Ea, q, rms.EmptyRateUncertainty()) elif isinstance(obj, PDepArrhenius): Ps = obj._pressures.value_si arrs = [to_rms(arr) for arr in obj.arrhenius] From e3344f517baaf98301b7199250b767be0c81a3f9 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 7 Dec 2021 16:56:29 -0500 Subject: [PATCH 029/109] filter out resonance structures that try to put - charges on Li fix resonance bug (squash) --- rmgpy/molecule/filtration.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/rmgpy/molecule/filtration.py b/rmgpy/molecule/filtration.py index 0ccdf991bc..dc310f036f 100644 --- a/rmgpy/molecule/filtration.py +++ b/rmgpy/molecule/filtration.py @@ -64,11 +64,14 @@ def filter_structures(mol_list, mark_unreactive=True, allow_expanded_octet=True, if not all([(mol.multiplicity == mol_list[0].multiplicity) for mol in mol_list]): raise ValueError("Cannot filter structures with different multiplicities!") + #Remove structures that try to put negative charges on metal ions + filtered_list = ionic_bond_filteration(mol_list) + # Get an octet deviation list - octet_deviation_list = get_octet_deviation_list(mol_list, allow_expanded_octet=allow_expanded_octet) + octet_deviation_list = get_octet_deviation_list(filtered_list, allow_expanded_octet=allow_expanded_octet) # Filter mol_list using the octet rule and the respective octet deviation list - filtered_list, charge_span_list = octet_filtration(mol_list, octet_deviation_list) + filtered_list, charge_span_list = octet_filtration(filtered_list, octet_deviation_list) # Filter by charge filtered_list = charge_filtration(filtered_list, charge_span_list) @@ -91,6 +94,21 @@ def filter_structures(mol_list, mark_unreactive=True, allow_expanded_octet=True, return filtered_list +def ionic_bond_filteration(mol_list): + """ + Returns a filtered list removing structures that put a negative charge on lithium + which is ionically bonded and thus cannot donate/recieve electrons covalently + """ + filtered_list = [] + for mol in mol_list: + for atom in mol.atoms: + if atom.is_lithium() and atom.charge < 0: + break + else: + filtered_list.append(mol) + + return filtered_list + def get_octet_deviation_list(mol_list, allow_expanded_octet=True): """ Returns the a list of octet deviations for a respective list of :class:Molecule objects @@ -113,7 +131,7 @@ def get_octet_deviation(mol, allow_expanded_octet=True): octet_deviation = 0 # This is the overall "score" for the molecule, summed across all non-H atoms for atom in mol.vertices: - if isinstance(atom, CuttingLabel) or atom.is_hydrogen(): + if isinstance(atom, CuttingLabel) or atom.is_hydrogen() or atom.is_lithium(): continue val_electrons = 2 * (int(atom.get_total_bond_order()) + atom.lone_pairs) + atom.radical_electrons if atom.is_carbon() or atom.is_nitrogen() or atom.is_oxygen(): From 8e855e6263efd594d0751d6c26ca057fb70371ef Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 13:04:37 -0500 Subject: [PATCH 030/109] ensure atomtypes get updated on loading in thermo entries --- rmgpy/data/thermo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rmgpy/data/thermo.py b/rmgpy/data/thermo.py index 26d2b20218..c357b5b230 100644 --- a/rmgpy/data/thermo.py +++ b/rmgpy/data/thermo.py @@ -608,10 +608,12 @@ def load_entry(self, index, label, molecule, thermo, reference=None, referenceTy Method for parsing entries in database files. Note that these argument names are retained for backward compatibility. """ + mol = Molecule().from_adjacency_list(molecule) + mol.update_atomtypes() entry = Entry( index=index, label=label, - item=Molecule().from_adjacency_list(molecule), + item=mol, data=thermo, reference=reference, reference_type=referenceType, @@ -666,6 +668,7 @@ def load_entry(self, molecule = Molecule().from_adjacency_list(molecule) except TypeError: molecule = Fragment().from_adjacency_list(molecule) + molecule.update_atomtypes() # Internal checks for adding entry to the thermo library if label in list(self.entries.keys()): From b03dd9831428ecaa3bfbe27ce940575fbe0b7c35 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 13:18:50 -0500 Subject: [PATCH 031/109] add solute data to KineticsModel and Arrhenius --- rmgpy/data/kinetics/database.py | 5 +++-- rmgpy/kinetics/arrhenius.pyx | 30 +++++++++++++++++++++--------- rmgpy/kinetics/model.pxd | 3 ++- rmgpy/kinetics/model.pyx | 8 +++++--- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/rmgpy/data/kinetics/database.py b/rmgpy/data/kinetics/database.py index 56c73ebf60..db3505f7c3 100644 --- a/rmgpy/data/kinetics/database.py +++ b/rmgpy/data/kinetics/database.py @@ -50,7 +50,7 @@ from rmgpy.molecule import Molecule, Group from rmgpy.reaction import Reaction, same_species_lists from rmgpy.species import Species - +from rmgpy.data.solvation import SoluteData ################################################################################ @@ -83,7 +83,8 @@ def __init__(self): 'SurfaceArrheniusBEP': SurfaceArrheniusBEP, 'SurfaceChargeTransfer': SurfaceChargeTransfer, 'R': constants.R, - 'ArrheniusBM': ArrheniusBM + 'ArrheniusBM': ArrheniusBM, + 'SoluteData': SoluteData, } self.global_context = {} diff --git a/rmgpy/kinetics/arrhenius.pyx b/rmgpy/kinetics/arrhenius.pyx index f24defb8cc..d049bd3b4d 100644 --- a/rmgpy/kinetics/arrhenius.pyx +++ b/rmgpy/kinetics/arrhenius.pyx @@ -58,15 +58,16 @@ cdef class Arrhenius(KineticsModel): `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data `comment` Information about the model (e.g. its source) =============== ============================================================= """ def __init__(self, A=None, n=0.0, Ea=None, T0=(1.0, "K"), Tmin=None, Tmax=None, Pmin=None, Pmax=None, - uncertainty=None, comment=''): + uncertainty=None, solute=None, comment=''): KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, - comment=comment) + solute=solute, comment=comment) self.A = A self.n = n self.Ea = Ea @@ -83,6 +84,7 @@ cdef class Arrhenius(KineticsModel): if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.solute: string += ', solute={0!r}'.format(self.solute) if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) string += ')' return string @@ -92,7 +94,7 @@ cdef class Arrhenius(KineticsModel): A helper function used when pickling an Arrhenius object. """ return (Arrhenius, (self.A, self.n, self.Ea, self.T0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, - self.uncertainty, self.comment)) + self.uncertainty, self.solute, self.comment)) property A: """The preexponential factor.""" @@ -196,6 +198,7 @@ cdef class Arrhenius(KineticsModel): self.T0 = (T0, "K") self.Tmin = (np.min(Tlist), "K") self.Tmax = (np.max(Tlist), "K") + self.solute = None self.comment = 'Fitted to {0:d} data points; dA = *|/ {1:g}, dn = +|- {2:g}, dEa = +|- {3:g} kJ/mol'.format( len(Tlist), exp(sqrt(cov[0, 0])), @@ -301,6 +304,7 @@ cdef class Arrhenius(KineticsModel): Pmin=self.Pmin, Pmax=self.Pmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment) return aep ################################################################################ @@ -323,15 +327,16 @@ cdef class ArrheniusEP(KineticsModel): `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data `comment` Information about the model (e.g. its source) =============== ============================================================= """ def __init__(self, A=None, n=0.0, alpha=0.0, E0=None, Tmin=None, Tmax=None, Pmin=None, Pmax=None, uncertainty=None, - comment=''): + solute=None, comment=''): KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, - comment=comment) + solute=solute, comment=comment) self.A = A self.n = n self.alpha = alpha @@ -348,6 +353,7 @@ cdef class ArrheniusEP(KineticsModel): if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) if self.uncertainty is not None: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.solute is not None: string += ', solute={0!r}'.format(self.solute) if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) string += ')' return string @@ -357,7 +363,7 @@ cdef class ArrheniusEP(KineticsModel): A helper function used when pickling an ArrheniusEP object. """ return (ArrheniusEP, (self.A, self.n, self.alpha, self.E0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, - self.uncertainty, self.comment)) + self.uncertainty, self.solute, self.comment)) property A: """The preexponential factor.""" @@ -428,6 +434,7 @@ cdef class ArrheniusEP(KineticsModel): Pmin=self.Pmin, Pmax=self.Pmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment, ) @@ -481,15 +488,16 @@ cdef class ArrheniusBM(KineticsModel): `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data `comment` Information about the model (e.g. its source) =============== ============================================================= """ def __init__(self, A=None, n=0.0, w0=(0.0, 'J/mol'), E0=None, Tmin=None, Tmax=None, Pmin=None, Pmax=None, - uncertainty=None, comment=''): + uncertainty=None, solute=None, comment=''): KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, - comment=comment) + solute=solute, comment=comment) self.A = A self.n = n self.w0 = w0 @@ -506,6 +514,7 @@ cdef class ArrheniusBM(KineticsModel): if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) if self.uncertainty is not None: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.solute is not None: string += ', solute={0!r}'.format(self.solute) if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) string += ')' return string @@ -515,7 +524,7 @@ cdef class ArrheniusBM(KineticsModel): A helper function used when pickling an ArrheniusEP object. """ return (ArrheniusBM, (self.A, self.n, self.w0, self.E0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, - self.uncertainty, self.comment)) + self.uncertainty, self.solute, self.comment)) property A: """The preexponential factor.""" @@ -586,6 +595,7 @@ cdef class ArrheniusBM(KineticsModel): Tmin=self.Tmin, Tmax=self.Tmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment, ) @@ -623,6 +633,7 @@ cdef class ArrheniusBM(KineticsModel): self.Tmin = rxn.kinetics.Tmin self.Tmax = rxn.kinetics.Tmax + self.solute = None self.comment = 'Fitted to {0} reaction at temperature: {1} K'.format(len(rxns), T) else: # define optimization function @@ -672,6 +683,7 @@ cdef class ArrheniusBM(KineticsModel): self.Tmin = (np.min(Ts), "K") self.Tmax = (np.max(Ts), "K") + self.solute = None self.comment = 'Fitted to {0} reactions at temperatures: {1}'.format(len(rxns), Ts) # fill in parameters diff --git a/rmgpy/kinetics/model.pxd b/rmgpy/kinetics/model.pxd index 9fa24cb767..9f2fa3f28c 100644 --- a/rmgpy/kinetics/model.pxd +++ b/rmgpy/kinetics/model.pxd @@ -29,7 +29,7 @@ cimport numpy as np from rmgpy.quantity cimport ScalarQuantity, ArrayQuantity from rmgpy.kinetics.uncertainties cimport RateUncertainty - +from rmgpy.data.solvation import SoluteData ################################################################################ cpdef str get_rate_coefficient_units_from_reaction_order(n_gas, n_surf=?) @@ -43,6 +43,7 @@ cdef class KineticsModel: cdef public ScalarQuantity _Tmin, _Tmax cdef public ScalarQuantity _Pmin, _Pmax cdef public RateUncertainty uncertainty + cdef public object solute cdef public str comment diff --git a/rmgpy/kinetics/model.pyx b/rmgpy/kinetics/model.pyx index 85760d1892..9624afc8e8 100644 --- a/rmgpy/kinetics/model.pyx +++ b/rmgpy/kinetics/model.pyx @@ -119,16 +119,18 @@ cdef class KineticsModel: `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Solute data for the transition state `comment` Information about the model (e.g. its source) =============== ============================================================ """ - def __init__(self, Tmin=None, Tmax=None, Pmin=None, Pmax=None, uncertainty=None, comment=''): + def __init__(self, Tmin=None, Tmax=None, Pmin=None, Pmax=None, uncertainty=None, solute=None, comment=''): self.Tmin = Tmin self.Tmax = Tmax self.Pmin = Pmin self.Pmax = Pmax + self.solute = solute self.uncertainty = uncertainty self.comment = comment @@ -138,13 +140,13 @@ cdef class KineticsModel: KineticsModel object. """ return 'KineticsModel(Tmin={0!r}, Tmax={1!r}, Pmin={2!r}, Pmax={3!r}, uncertainty={4!r}, comment="""{5}""")'.format( - self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.uncertainty, self.comment) + self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.solute, self.uncertainty, self.comment) def __reduce__(self): """ A helper function used when pickling a KineticsModel object. """ - return (KineticsModel, (self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.uncertainty, self.comment)) + return (KineticsModel, (self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.solute, self.uncertainty, self.comment)) property Tmin: """The minimum temperature at which the model is valid, or ``None`` if not defined.""" From d03c9b0b3f81b570a477e13319ea1d7045362d73 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Fri, 9 Sep 2022 16:06:56 -0700 Subject: [PATCH 032/109] add index of refraction to SolventData objects --- rmgpy/data/solvation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index 892a81e53b..ac162ea780 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -138,6 +138,7 @@ def save_entry(f, entry): f.write(' alpha = {0!r},\n'.format(entry.data.alpha)) f.write(' beta = {0!r},\n'.format(entry.data.beta)) f.write(' eps = {0!r},\n'.format(entry.data.eps)) + f.write(' n = {0!r},\n'.format(entry.data.n)) f.write(' name_in_coolprop = "{0!s}",\n'.format(entry.data.name_in_coolprop)) f.write(' ),\n') elif entry.data is None: @@ -361,7 +362,7 @@ class SolventData(object): def __init__(self, s_h=None, b_h=None, e_h=None, l_h=None, a_h=None, c_h=None, s_g=None, b_g=None, e_g=None, l_g=None, a_g=None, c_g=None, A=None, B=None, - C=None, D=None, E=None, alpha=None, beta=None, eps=None, name_in_coolprop=None): + C=None, D=None, E=None, alpha=None, beta=None, eps=None, n=None, name_in_coolprop=None): self.s_h = s_h self.b_h = b_h self.e_h = e_h @@ -385,6 +386,8 @@ def __init__(self, s_h=None, b_h=None, e_h=None, l_h=None, a_h=None, self.beta = beta # This is the dielectric constant self.eps = eps + #This is the index of refraction + self.n = n # This corresponds to the solvent's name in CoolProp. CoolProp is an external package used for # fluid property calculation. If the solvent is not available in CoolProp, this is set to None self.name_in_coolprop = name_in_coolprop From 42c96a754a325f3bd59bc213268ca12ae3f6efcd Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 13:20:09 -0500 Subject: [PATCH 033/109] added ArrheniusChargeTransfer kinetics type --- rmgpy/kinetics/arrhenius.pxd | 29 ++++ rmgpy/kinetics/arrhenius.pyx | 261 +++++++++++++++++++++++++++++++++++ rmgpy/yml.py | 17 ++- 3 files changed, 302 insertions(+), 5 deletions(-) diff --git a/rmgpy/kinetics/arrhenius.pxd b/rmgpy/kinetics/arrhenius.pxd index 21e44a3be3..ee20bd24ed 100644 --- a/rmgpy/kinetics/arrhenius.pxd +++ b/rmgpy/kinetics/arrhenius.pxd @@ -128,4 +128,33 @@ cdef class MultiPDepArrhenius(PDepKineticsModel): cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef change_rate(self, double factor) + +################################################################################ +cdef class ArrheniusChargeTransfer(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ScalarQuantity _Ea + cdef public ScalarQuantity _T0 + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _alpha + cdef public ScalarQuantity _electrons + + cpdef double get_activation_energy_from_potential(self, double V=?, bint non_negative=?) + + cpdef double get_rate_coefficient(self, double T, double V=?) except -1 + + cpdef change_rate(self, double factor) + + cpdef change_t0(self, double T0) + + cpdef change_v0(self, double V0) + + cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=?, np.ndarray weights=?, bint three_params=?) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef change_rate(self, double factor) diff --git a/rmgpy/kinetics/arrhenius.pyx b/rmgpy/kinetics/arrhenius.pyx index d049bd3b4d..47dcea22c9 100644 --- a/rmgpy/kinetics/arrhenius.pyx +++ b/rmgpy/kinetics/arrhenius.pyx @@ -1109,6 +1109,267 @@ cdef class MultiPDepArrhenius(PDepKineticsModel): for i, arr in enumerate(self.arrhenius): arr.set_cantera_kinetics(ct_reaction[i], species_list) +################################################################################ + +cdef class ArrheniusChargeTransfer(KineticsModel): + + """ + A kinetics model for surface charge transfer reactions + + It is very similar to the :class:`SurfaceArrhenius`, but the Ea is potential-dependent + + + The attributes are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `T0` The reference temperature + `n` The temperature exponent + `Ea` The activation energy + `electrons` The stochiometry coeff for electrons (negative if reactant, positive if product) + `V0` The reference potential + `alpha` The charge transfer coefficient + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` The transition state solute data + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, Ea=None, V0=None, alpha=0.5, electrons=-1, T0=(1.0, "K"), Tmin=None, Tmax=None, + Pmin=None, Pmax=None, solute=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, solute=solute, uncertainty=uncertainty, + comment=comment) + + self.alpha = alpha + self.A = A + self.n = n + self.Ea = Ea + self.T0 = T0 + self.electrons = electrons + self.V0 = V0 + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Arrhenius object. + """ + string = 'ArrheniusChargeTransfer(A={0!r}, n={1!r}, Ea={2!r}, V0={3!r}, alpha={4!r}, electrons={5!r}, T0={6!r}'.format( + self.A, self.n, self.Ea, self.V0, self.alpha, self.electrons, self.T0) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.solute: string += ', solute={0!r}'.format(self.solute) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a ArrheniusChargeTransfer object. + """ + return (ArrheniusChargeTransfer, (self.A, self.n, self.Ea, self.V0, self.alpha, self.electrons, self.T0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.solute, self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.SurfaceRateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property Ea: + """The activation energy.""" + def __get__(self): + return self._Ea + def __set__(self, value): + self._Ea = quantity.Energy(value) + + property T0: + """The reference temperature.""" + def __get__(self): + return self._T0 + def __set__(self, value): + self._T0 = quantity.Temperature(value) + + property V0: + """The reference potential.""" + def __get__(self): + return self._V0 + def __set__(self, value): + self._V0 = quantity.Potential(value) + + property electrons: + """The number of electrons transferred.""" + def __get__(self): + return self._electrons + def __set__(self, value): + self._electrons = quantity.Dimensionless(value) + + property alpha: + """The charge transfer coefficient.""" + def __get__(self): + return self._alpha + def __set__(self, value): + self._alpha = quantity.Dimensionless(value) + + cpdef double get_activation_energy_from_potential(self, double V=0.0, bint non_negative=True): + """ + Return the effective activation energy (in J/mol) at specificed potential (in Volts). + """ + cdef double electrons, alpha, Ea, V0 + + electrons = self._electrons.value_si + alpha = self._alpha.value_si + Ea = self._Ea.value_si + V0 = self._V0.value_si + + Ea -= alpha * electrons * constants.F * (V-V0) + + if non_negative is True: + if Ea < 0: + Ea = 0.0 + + return Ea + + cpdef double get_rate_coefficient(self, double T, double V=0.0) except -1: + """ + Return the rate coefficient in the appropriate combination of m^2, + mol, and s at temperature `T` in K. + """ + cdef double A, n, V0, T0, Ea + + A = self._A.value_si + n = self._n.value_si + V0 = self._V0.value_si + T0 = self._T0.value_si + + if V != V0: + Ea = self.get_activation_energy_from_potential(V) + else: + Ea = self._Ea.value_si + + return A * (T / T0) ** n * exp(-Ea / (constants.R * T)) + + cpdef change_t0(self, double T0): + """ + Changes the reference temperature used in the exponent to `T0` in K, + and adjusts the preexponential factor accordingly. + """ + self._A.value_si /= (self._T0.value_si / T0) ** self._n.value_si + self._T0.value_si = T0 + + cpdef change_v0(self, double V0): + """ + Changes the reference potential to `V0` in volts, and adjusts the + activation energy `Ea` accordingly. + """ + + self._Ea.value_si = self.get_activation_energy_from_potential(V0) + self._V0.value_si = V0 + + cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=1, + np.ndarray weights=None, bint three_params=False): + """ + Fit the Arrhenius parameters to a set of rate coefficient data `klist` + in units of `kunits` corresponding to a set of temperatures `Tlist` in + K. A linear least-squares fit is used, which guarantees that the + resulting parameters provide the best possible approximation to the + data. + """ + import scipy.stats + if not all(np.isfinite(klist)): + raise ValueError("Rates must all be finite, not inf or NaN") + if any(klist<0): + if not all(klist<0): + raise ValueError("Rates must all be positive or all be negative.") + rate_sign_multiplier = -1 + klist = -1 * klist + else: + rate_sign_multiplier = 1 + + assert len(Tlist) == len(klist), "length of temperatures and rates must be the same" + if len(Tlist) < 3 + three_params: + raise KineticsError('Not enough degrees of freedom to fit this Arrhenius expression') + if three_params: + A = np.zeros((len(Tlist), 3), np.float64) + A[:, 0] = np.ones_like(Tlist) + A[:, 1] = np.log(Tlist / T0) + A[:, 2] = -1.0 / constants.R / Tlist + else: + A = np.zeros((len(Tlist), 2), np.float64) + A[:, 0] = np.ones_like(Tlist) + A[:, 1] = -1.0 / constants.R / Tlist + b = np.log(klist) + if weights is not None: + for n in range(b.size): + A[n, :] *= weights[n] + b[n] *= weights[n] + x, residues, rank, s = np.linalg.lstsq(A, b, rcond=RCOND) + + # Determine covarianace matrix to obtain parameter uncertainties + count = klist.size + cov = residues[0] / (count - 3) * np.linalg.inv(np.dot(A.T, A)) + t = scipy.stats.t.ppf(0.975, count - 3) + + if not three_params: + x = np.array([x[0], 0, x[1]]) + cov = np.array([[cov[0, 0], 0, cov[0, 1]], [0, 0, 0], [cov[1, 0], 0, cov[1, 1]]]) + + self.A = (rate_sign_multiplier * exp(x[0]), kunits) + self.n = x[1] + self.Ea = (x[2] * 0.001, "kJ/mol") + self.T0 = (T0, "K") + self.Tmin = (np.min(Tlist), "K") + self.Tmax = (np.max(Tlist), "K") + self.solute = None, + self.comment = 'Fitted to {0:d} data points; dA = *|/ {1:g}, dn = +|- {2:g}, dEa = +|- {3:g} kJ/mol'.format( + len(Tlist), + exp(sqrt(cov[0, 0])), + sqrt(cov[1, 1]), + sqrt(cov[2, 2]) * 0.001, + ) + + return self + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, ArrheniusChargeTransfer): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.Ea.equals(other_kinetics.Ea) or not self.T0.equals(other_kinetics.T0) + or not self.alpha.equals(other_kinetics.alpha) or not self.electrons.equals(other_kinetics.electrons) + or not self.V0.equals(other_kinetics.V0)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor in Arrhenius expression by multiplying it by a ``factor``. + """ + self._A.value_si *= factor def get_w0(actions, rxn): """ calculates the w0 for Blower Masel kinetics by calculating wf (total bond energy of bonds formed) diff --git a/rmgpy/yml.py b/rmgpy/yml.py index 5d78934a80..76db07386a 100644 --- a/rmgpy/yml.py +++ b/rmgpy/yml.py @@ -40,7 +40,7 @@ from rmgpy.reaction import Reaction from rmgpy.thermo.nasa import NASAPolynomial, NASA from rmgpy.thermo.wilhoit import Wilhoit -from rmgpy.kinetics.arrhenius import Arrhenius, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius +from rmgpy.kinetics.arrhenius import Arrhenius, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, ArrheniusChargeTransfer from rmgpy.kinetics.falloff import Troe, ThirdBody, Lindemann from rmgpy.kinetics.chebyshev import Chebyshev from rmgpy.data.solvation import SolventData @@ -148,14 +148,21 @@ def obj_to_dict(obj, spcs, names=None, label="solvent"): result_dict["A"] = obj.A.value_si result_dict["Ea"] = obj.Ea.value_si result_dict["n"] = obj.n.value_si + elif isinstance(obj, ArrheniusChargeTransfer): + obj.change_t0(1.0) + obj.change_v0(0.0) + result_dict["type"] = "Arrheniusq" + result_dict["A"] = obj.A.value_si + result_dict["Ea"] = obj.Ea.value_si + result_dict["n"] = obj.n.value_si + result_dict["q"] = obj._alpha.value_si*obj._electrons.value_si elif isinstance(obj, SurfaceChargeTransfer): - result_dict["type"] = "SurfaceChargeTransfer" + obj.change_v0(0.0) + result_dict["type"] = "Arrheniusq" result_dict["A"] = obj.A.value_si result_dict["Ea"] = obj.Ea.value_si result_dict["n"] = obj.n.value_si - result_dict["electrons"] = obj.electrons.value_si - result_dict["V0"] = obj.V0.value_si - result_dict["alpha"] = obj.alpha.value_si + result_dict["q"] = obj._alpha.value_si*obj._electrons.value_si elif isinstance(obj, StickingCoefficient): obj.change_t0(1.0) result_dict["type"] = "StickingCoefficient" From cf1e222149d8b3feb49f1d02c9561d23d5592e36 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 13:22:36 -0500 Subject: [PATCH 034/109] added ArrheniusChargeTransferBM type --- rmgpy/kinetics/arrhenius.pxd | 22 +++ rmgpy/kinetics/arrhenius.pyx | 319 +++++++++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) diff --git a/rmgpy/kinetics/arrhenius.pxd b/rmgpy/kinetics/arrhenius.pxd index ee20bd24ed..08f3b95010 100644 --- a/rmgpy/kinetics/arrhenius.pxd +++ b/rmgpy/kinetics/arrhenius.pxd @@ -157,4 +157,26 @@ cdef class ArrheniusChargeTransfer(KineticsModel): cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 +################################################################################ + +cdef class ArrheniusChargeTransferBM(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ScalarQuantity _E0 + cdef public ScalarQuantity _w0 + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _alpha + cdef public ScalarQuantity _electrons + + cpdef change_v0(self, double V0) + + cpdef double get_activation_energy(self, double dGrxn) except -1 + + cpdef double get_rate_coefficient_from_potential(self, double T, double V, double dGrxn) except -1 + + cpdef ArrheniusChargeTransfer to_arrhenius_charge_transfer(self, double dGrxn) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + cpdef change_rate(self, double factor) diff --git a/rmgpy/kinetics/arrhenius.pyx b/rmgpy/kinetics/arrhenius.pyx index 47dcea22c9..db654c21ea 100644 --- a/rmgpy/kinetics/arrhenius.pyx +++ b/rmgpy/kinetics/arrhenius.pyx @@ -37,6 +37,7 @@ import rmgpy.quantity as quantity from rmgpy.exceptions import KineticsError from rmgpy.kinetics.uncertainties import rank_accuracy_map from rmgpy.molecule.molecule import Bond +import logging # Prior to numpy 1.14, `numpy.linalg.lstsq` does not accept None as a value RCOND = -1 if int(np.__version__.split('.')[1]) < 14 else None @@ -1370,6 +1371,324 @@ cdef class ArrheniusChargeTransfer(KineticsModel): Changes A factor in Arrhenius expression by multiplying it by a ``factor``. """ self._A.value_si *= factor + +cdef class ArrheniusChargeTransferBM(KineticsModel): + """ + A kinetics model based on the (modified) Arrhenius equation, using the + Evans-Polanyi equation to determine the activation energy. The attributes + are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `n` The temperature exponent + `w0` The average of the bond dissociation energies of the bond formed and the bond broken + `E0` The activation energy for a thermoneutral reaction + `electrons` The stochiometry coeff for electrons (negative if reactant, positive if product) + `V0` The reference potential + `alpha` The charge transfer coefficient + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, w0=(0.0, 'J/mol'), E0=None, V0=(0.0,'V'), alpha=0.5, electrons=-1, Tmin=None, Tmax=None, + Pmin=None, Pmax=None, solute=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, solute=solute, uncertainty=uncertainty, + comment=comment) + + self.alpha = alpha + self.A = A + self.n = n + self.w0 = w0 + self.E0 = E0 + self.electrons = electrons + self.V0 = V0 + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Arrhenius object. + """ + string = 'ArrheniusChargeTransferBM(A={0!r}, n={1!r}, w0={2!r}, E0={3!r}, V0={4!r}, alpha={5!r}, electrons={6!r}'.format( + self.A, self.n, self.w0, self.E0, self.V0, self.alpha, self.electrons) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.solute: string += ', solute={0!r}'.format(self.solute) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a ArrheniusChargeTransfer object. + """ + return (ArrheniusChargeTransferBM, (self.A, self.n, self.w0, self.E0, self.V0, self.alpha, self.electrons, self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.solute, self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.SurfaceRateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property w0: + """The average of the bond dissociation energies of the bond formed and the bond broken.""" + def __get__(self): + return self._w0 + def __set__(self, value): + self._w0 = quantity.Energy(value) + + property E0: + """The activation energy.""" + def __get__(self): + return self._E0 + def __set__(self, value): + self._E0 = quantity.Energy(value) + + property V0: + """The reference potential.""" + def __get__(self): + return self._V0 + def __set__(self, value): + self._V0 = quantity.Potential(value) + + property electrons: + """The number of electrons transferred.""" + def __get__(self): + return self._electrons + def __set__(self, value): + self._electrons = quantity.Dimensionless(value) + + property alpha: + """The charge transfer coefficient.""" + def __get__(self): + return self._alpha + def __set__(self, value): + self._alpha = quantity.Dimensionless(value) + + cpdef change_v0(self, double V0): + """ + Changes the reference potential to `V0` in volts, and adjusts the + activation energy `E0` accordingly. + """ + + self._E0.value_si = self.get_activation_energy_from_potential(V0,0.0) + self._V0.value_si = V0 + + cpdef double get_rate_coefficient(self, double T, double dHrxn=0.0) except -1: + """ + Return the rate coefficient in the appropriate combination of m^3, + mol, and s at temperature `T` in K and enthalpy of reaction `dHrxn` + in J/mol. + """ + cdef double A, n, Ea + Ea = self.get_activation_energy(dHrxn) + A = self._A.value_si + n = self._n.value_si + return A * T ** n * exp(-Ea / (constants.R * T)) + + cpdef double get_activation_energy(self, double dHrxn) except -1: + """ + Return the activation energy in J/mol corresponding to the given + enthalpy of reaction `dHrxn` in J/mol. + """ + cdef double w0, E0 + E0 = self._E0.value_si + if dHrxn < -4 * self._E0.value_si: + return 0.0 + elif dHrxn > 4 * self._E0.value_si: + return dHrxn + else: + w0 = self._w0.value_si + Vp = 2 * w0 * (2 * w0 + 2 * E0) / (2 * w0 - 2 * E0) + return (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) ** 2 / (Vp ** 2 - (2 * w0) ** 2 + dHrxn ** 2) + + cpdef double get_rate_coefficient_from_potential(self, double T, double V, double dHrxn) except -1: + """ + Return the rate coefficient in the appropriate combination of m^3, + mol, and s at temperature `T` in K, potential `V` in volts, and + heat of reaction `dHrxn` in J/mol. + """ + cdef double A, n, Ea + Ea = self.get_activation_energy_from_potential(V,dHrxn) + Ea -= self._alpha.value_si * self._electrons.value_si * constants.F * (V-self._V0.value_si) + A = self._A.value_si + n = self._n.value_si + return A * T ** n * exp(-Ea / (constants.R * T)) + + def fit_to_reactions(self, rxns, w0=None, recipe=None, Ts=None): + """ + Fit an ArrheniusBM model to a list of reactions at the given temperatures, + w0 must be either given or estimated using the family object + """ + assert w0 is not None or recipe is not None, 'either w0 or recipe must be specified' + + for rxn in rxns: + if rxn.kinetics._V0.value_si != 0.0: + rxn.kinetics.change_v0(0.0) + + if Ts is None: + Ts = [300.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0, 1100.0, 1200.0, 1500.0] + if w0 is None: + #estimate w0 + w0s = get_w0s(recipe, rxns) + w0 = sum(w0s) / len(w0s) + + if len(rxns) == 1: + T = 1000.0 + rxn = rxns[0] + dHrxn = rxn.get_enthalpy_of_reaction(T) + A = rxn.kinetics.A.value_si + n = rxn.kinetics.n.value_si + Ea = rxn.kinetics.Ea.value_si + + def kfcn(E0): + Vp = 2 * w0 * (2 * w0 + 2 * E0) / (2 * w0 - 2 * E0) + out = Ea - (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) * (Vp - 2 * w0 + dHrxn) / (Vp * Vp - (2 * w0) * (2 * w0) + dHrxn * dHrxn) + return out + + if abs(dHrxn) > 4 * w0 / 10.0: + E0 = w0 / 10.0 + else: + E0 = fsolve(kfcn, w0 / 10.0)[0] + + self.Tmin = rxn.kinetics.Tmin + self.Tmax = rxn.kinetics.Tmax + self.comment = 'Fitted to {0} reaction at temperature: {1} K'.format(len(rxns), T) + else: + # define optimization function + def kfcn(xs, lnA, n, E0): + T = xs[:,0] + dHrxn = xs[:,1] + Vp = 2 * w0 * (2 * w0 + 2 * E0) / (2 * w0 - 2 * E0) + Ea = (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) * (Vp - 2 * w0 + dHrxn) / (Vp * Vp - (2 * w0) * (2 * w0) + dHrxn * dHrxn) + Ea = np.where(dHrxn< -4.0*E0, 0.0, Ea) + Ea = np.where(dHrxn > 4.0*E0, dHrxn, Ea) + return lnA + np.log(T ** n * np.exp(-Ea / (8.314 * T))) + + # get (T,dHrxn(T)) -> (Ln(k) mappings + xdata = [] + ydata = [] + sigmas = [] + for rxn in rxns: + # approximately correct the overall uncertainties to std deviations + s = rank_accuracy_map[rxn.rank].value_si/2.0 + for T in Ts: + xdata.append([T, rxn.get_enthalpy_of_reaction(T)]) + ydata.append(np.log(rxn.get_rate_coefficient(T))) + + sigmas.append(s / (8.314 * T)) + + xdata = np.array(xdata) + ydata = np.array(ydata) + + # fit parameters + boo = True + xtol = 1e-8 + ftol = 1e-8 + while boo: + boo = False + try: + params = curve_fit(kfcn, xdata, ydata, sigma=sigmas, p0=[1.0, 1.0, w0 / 10.0], xtol=xtol, ftol=ftol) + except RuntimeError: + if xtol < 1.0: + boo = True + xtol *= 10.0 + ftol *= 10.0 + else: + raise ValueError("Could not fit BM arrhenius to reactions with xtol<1.0") + + lnA, n, E0 = params[0].tolist() + A = np.exp(lnA) + + self.Tmin = (np.min(Ts), "K") + self.Tmax = (np.max(Ts), "K") + self.comment = 'Fitted to {0} reactions at temperatures: {1}'.format(len(rxns), Ts) + + # fill in parameters + A_units = ['', 's^-1', 'm^3/(mol*s)', 'm^6/(mol^2*s)'] + order = len(rxns[0].reactants) + self.A = (A, A_units[order]) + + self.n = n + self.w0 = (w0, 'J/mol') + self.E0 = (E0, 'J/mol') + self._V0.value_si = 0.0 + self.electrons = rxns[0].electrons + + return self + + cpdef ArrheniusChargeTransfer to_arrhenius_charge_transfer(self, double dHrxn): + """ + Return an :class:`ArrheniusChargeTransfer` instance of the kinetics model using the + given heat of reaction `dHrxn` to determine the activation energy. + """ + return ArrheniusChargeTransfer( + A=self.A, + n=self.n, + electrons=self.electrons, + Ea=(self.get_activation_energy(dHrxn) * 0.001, "kJ/mol"), + V0=self.V0, + T0=(1, "K"), + Tmin=self.Tmin, + Tmax=self.Tmax, + Pmin=self.Pmin, + Pmax=self.Pmax, + uncertainty=self.uncertainty, + solute=self.solute, + comment=self.comment, + ) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, ArrheniusChargeTransferBM): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.E0.equals(other_kinetics.E0) or not self.w0.equals(other_kinetics.w0) + or not self.alpha.equals(other_kinetics.alpha) + or not self.electrons.equals(other_kinetics.electrons) or not self.V0.equals(other_kinetics.V0)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + + def set_cantera_kinetics(self, ct_reaction, species_list): + """ + Sets a cantera ElementaryReaction() object with the modified Arrhenius object + converted to an Arrhenius form. + """ + raise NotImplementedError('set_cantera_kinetics() is not implemented for ArrheniusEP class kinetics.') + def get_w0(actions, rxn): """ calculates the w0 for Blower Masel kinetics by calculating wf (total bond energy of bonds formed) From a490666ab24d4cc603c7f45bf0dcc3c11e1c255a Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 13:23:19 -0500 Subject: [PATCH 035/109] add ChargeTransfer types to kinetics/__init__.py --- rmgpy/kinetics/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rmgpy/kinetics/__init__.py b/rmgpy/kinetics/__init__.py index fe012a8642..2f3d3626d1 100644 --- a/rmgpy/kinetics/__init__.py +++ b/rmgpy/kinetics/__init__.py @@ -29,7 +29,8 @@ from rmgpy.kinetics.model import KineticsModel, PDepKineticsModel, TunnelingModel, \ get_rate_coefficient_units_from_reaction_order, get_reaction_order_from_rate_coefficient_units -from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, ArrheniusBM +from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, \ + ArrheniusBM, ArrheniusChargeTransfer, ArrheniusChargeTransferBM from rmgpy.kinetics.chebyshev import Chebyshev from rmgpy.kinetics.falloff import ThirdBody, Lindemann, Troe from rmgpy.kinetics.kineticsdata import KineticsData, PDepKineticsData From a3bf77159d8d19fb9d3773d480bf54d66d85114d Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 13:27:34 -0500 Subject: [PATCH 036/109] handle ChargeTransfer kinetics with in reaction.py --- rmgpy/reaction.py | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index f7d7b2cfea..b66e89538b 100644 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -53,7 +53,7 @@ from rmgpy.exceptions import ReactionError, KineticsError from rmgpy.kinetics import KineticsData, ArrheniusBM, ArrheniusEP, ThirdBody, Lindemann, Troe, Chebyshev, \ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, get_rate_coefficient_units_from_reaction_order, \ - SurfaceArrheniusBEP, StickingCoefficientBEP + SurfaceArrheniusBEP, StickingCoefficientBEP, ArrheniusChargeTransfer, ArrheniusChargeTransferBM from rmgpy.kinetics.arrhenius import Arrhenius # Separate because we cimport from rmgpy.kinetics.arrhenius from rmgpy.kinetics.surface import SurfaceArrhenius, StickingCoefficient, SurfaceChargeTransfer, SurfaceChargeTransferBEP # Separate because we cimport from rmgpy.kinetics.surface from rmgpy.kinetics.diffusionLimited import diffusion_limiter @@ -1015,7 +1015,18 @@ def fix_barrier_height(self, force_positive=False): logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol.".format( self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) self.kinetics.Ea.value_si = Ea - if isinstance(self.kinetics, (Arrhenius, StickingCoefficient)): # SurfaceArrhenius is a subclass of Arrhenius + if isinstance(self.kinetics, ArrheniusChargeTransferBM): + Ea = self.kinetics.E0.value_si # temporarily using Ea to store the intrinsic barrier height E0 + self.kinetics = self.kinetics.to_arrhenius_charge_transfer(H298) + if self.kinetics.Ea.value_si < 0.0 and self.kinetics.Ea.value_si < Ea: + # Calculated Ea (from Evans-Polanyi) is negative AND below than the intrinsic E0 + Ea = min(0.0, Ea) # (the lowest we want it to be) + self.kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol.".format( + self.kinetics.Ea.value_si / 1000., Ea / 1000.) + logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol.".format( + self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) + self.kinetics.Ea.value_si = Ea + if isinstance(self.kinetics, (Arrhenius, StickingCoefficient, ArrheniusChargeTransfer)): # SurfaceArrhenius is a subclass of Arrhenius Ea = self.kinetics.Ea.value_si if H0 >= 0 and Ea < H0: self.kinetics.Ea.value_si = H0 @@ -1135,7 +1146,30 @@ def reverse_surface_charge_transfer_rate(self, k_forward, reverse_units, Tmin=No klist = np.zeros_like(Tlist) for i in range(len(Tlist)): klist[i] = kf.get_rate_coefficient(Tlist[i],V0) / self.get_equilibrium_constant(Tlist[i],V0) - kr = SurfaceChargeTransfer(alpha=1-kf.alpha.value, electrons=-1*self.electrons, V0=(V0,'V')) + kr = SurfaceChargeTransfer(alpha=kf.alpha.value, electrons=-1*self.electrons, V0=(V0,'V')) + kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + return kr + + def reverse_arrhenius_charge_transfer_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): + """ + Reverses the given k_forward, which must be a SurfaceChargeTransfer type. + You must supply the correct units for the reverse rate. + The equilibrium constant is evaluated from the current reaction instance (self). + """ + cython.declare(Tlist=np.ndarray, klist=np.ndarray, i=cython.int, V0=cython.double) + kf = k_forward + if not isinstance(kf, ArrheniusChargeTransfer): # Only reverse SurfaceChargeTransfer rates + raise TypeError(f'Expected a ArrheniusChargeTransfer object for k_forward but received {kf}') + if Tmin is not None and Tmax is not None: + Tlist = 1.0 / np.linspace(1.0 / Tmax.value, 1.0 / Tmin.value, 50) + else: + Tlist = np.linspace(298, 500, 30) + + V0 = self.kinetics.V0.value_si + klist = np.zeros_like(Tlist) + for i in range(len(Tlist)): + klist[i] = kf.get_rate_coefficient(Tlist[i],V0) / self.get_equilibrium_constant(Tlist[i],V0) + kr = ArrheniusChargeTransfer(alpha=kf.alpha.value, electrons=-1*self.electrons, V0=(V0,'V')) kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) return kr @@ -1166,6 +1200,7 @@ def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, T Lindemann.__name__, Troe.__name__, StickingCoefficient.__name__, + ArrheniusChargeTransfer.__name__, ) # Get the units for the reverse rate coefficient @@ -1185,6 +1220,9 @@ def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, T if isinstance(kf, SurfaceChargeTransfer): return self.reverse_surface_charge_transfer_rate(kf, kunits, Tmin, Tmax) + elif isinstance(kf, ArrheniusChargeTransfer): + return self.reverse_arrhenius_charge_transfer_rate(kf, kunits, Tmin, Tmax) + elif isinstance(kf, KineticsData): Tlist = kf.Tdata.value_si @@ -1413,9 +1451,6 @@ def is_balanced(self): elif self.electrons > 0: products_net_charge -= self.electrons - if reactants_net_charge != products_net_charge: - return False - return True def generate_pairs(self): From 1ee3cf06aa1070ac166672768478c15b94950c1e Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 18 Jun 2022 12:47:39 -0700 Subject: [PATCH 037/109] handle ArrheniusChargeTransfer => Arrheniusq --- rmgpy/rmg/reactors.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index bbfab61df8..be79ebc9bb 100644 --- a/rmgpy/rmg/reactors.py +++ b/rmgpy/rmg/reactors.py @@ -55,13 +55,14 @@ from diffeqpy import de from julia import Main +from rmgpy import constants from rmgpy.species import Species from rmgpy.molecule.fragment import Fragment from rmgpy.reaction import Reaction from rmgpy.thermo.nasa import NASAPolynomial, NASA from rmgpy.thermo.wilhoit import Wilhoit from rmgpy.thermo.thermodata import ThermoData -from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, ArrheniusBM, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius +from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, ArrheniusBM, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, ArrheniusChargeTransfer from rmgpy.kinetics.kineticsdata import KineticsData from rmgpy.kinetics.falloff import Troe, ThirdBody, Lindemann from rmgpy.kinetics.chebyshev import Chebyshev @@ -590,6 +591,16 @@ def to_rms(obj, species_names=None, rms_species_list=None, rmg_species=None): n = obj._n.value_si Ea = obj._Ea.value_si return rms.Arrhenius(A, n, Ea, rms.EmptyRateUncertainty()) + elif isinstance(obj, ArrheniusChargeTransfer): + A = obj._A.value_si + if obj._T0.value_si != 1.0: + A /= ((obj._T0.value_si) ** obj._n.value_si) + if obj._V0.value_si != 0.0: + A *= np.exp(obj._alpha.value_si*obj._electrons.value_si*constants.F*obj.V0.value_si) + n = obj._n.value_si + Ea = obj._Ea.value_si + q = obj._alpha.value_si*obj._electrons.value_si + return rms.Arrheniusq(A, n, Ea, q, rms.EmptyRateUncertainty()) elif isinstance(obj, SurfaceChargeTransfer): A = obj._A.value_si if obj._T0.value_si != 1.0: From 7cba51163032fe9cdf92c26b29f60f52e727e3fe Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 14:41:28 -0500 Subject: [PATCH 038/109] add charge transfer types to database context --- rmgpy/data/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rmgpy/data/base.py b/rmgpy/data/base.py index a0a4d51444..e6d0bc249e 100644 --- a/rmgpy/data/base.py +++ b/rmgpy/data/base.py @@ -42,6 +42,7 @@ from rmgpy.data.reference import Reference, Article, Book, Thesis from rmgpy.exceptions import DatabaseError, InvalidAdjacencyListError from rmgpy.kinetics.uncertainties import RateUncertainty +from rmgpy.kinetics.arrhenius import ArrheniusChargeTransfer, ArrheniusChargeTransferBM from rmgpy.molecule import Molecule, Group @@ -228,6 +229,8 @@ def load(self, path, local_context=None, global_context=None): local_context['shortDesc'] = self.short_desc local_context['longDesc'] = self.long_desc local_context['RateUncertainty'] = RateUncertainty + local_context['ArrheniusChargeTransfer'] = ArrheniusChargeTransfer + local_context['ArrheniusChargeTransferBM'] = ArrheniusChargeTransferBM local_context['metal'] = self.metal local_context['site'] = self.site local_context['facet'] = self.facet From 81c69969877aa140d4a7435050eafa58accdbc09 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 14:40:52 -0500 Subject: [PATCH 039/109] enable use of non-surface charge transfer families --- rmgpy/data/kinetics/family.py | 262 +++++++++++++++++++--------------- 1 file changed, 149 insertions(+), 113 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 96d30d312d..2ec538e8ec 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -55,7 +55,8 @@ from rmgpy.exceptions import ActionError, DatabaseError, InvalidActionError, KekulizationError, KineticsError, \ ForbiddenStructureException, UndeterminableKineticsError from rmgpy.kinetics import Arrhenius, SurfaceArrhenius, SurfaceArrheniusBEP, StickingCoefficient, \ - StickingCoefficientBEP, ArrheniusBM, SurfaceChargeTransfer + StickingCoefficientBEP, ArrheniusBM, SurfaceChargeTransfer, ArrheniusChargeTransfer, \ + ArrheniusChargeTransferBM from rmgpy.kinetics.uncertainties import RateUncertainty, rank_accuracy_map from rmgpy.molecule import Bond, GroupBond, Group, Molecule from rmgpy.molecule.atomtype import ATOMTYPES @@ -318,7 +319,7 @@ def _apply(self, struct, forward, unique): if info < 1: raise InvalidActionError('Attempted to change a nonexistent bond.') # If we do not have a bond, it might be because we are trying to change a vdW bond - # Lets see if one of that atoms is a surface site, + # Lets see if one of that atoms is a surface site, # If we have a surface site, we will make a single bond, then change it by info - 1 is_vdW_bond = False for atom in (atom1, atom2): @@ -458,8 +459,8 @@ def apply_reverse(self, struct, unique=True): class KineticsFamily(Database): """ - A class for working with an RMG kinetics family: a set of reactions with - similar chemistry, and therefore similar reaction rates. The attributes + A class for working with an RMG kinetics family: a set of reactions with + similar chemistry, and therefore similar reaction rates. The attributes are: =================== =============================== ======================== @@ -550,11 +551,11 @@ def distribute_tree_distances(self): def load(self, path, local_context=None, global_context=None, depository_labels=None): """ Load a kinetics database from a file located at `path` on disk. - + If `depository_labels` is a list, eg. ['training','PrIMe'], then only those depositories are loaded, and they are searched in that order when generating kinetics. - + If depository_labels is None then load 'training' first then everything else. If depository_labels is not None then load in the order specified in depository_labels. """ @@ -651,7 +652,7 @@ def load(self, path, local_context=None, global_context=None, depository_labels= # depository and add them to the RMG rate rules by default: depository_labels = ['training'] if depository_labels: - # If there are depository labels, load them in the order specified, but + # If there are depository labels, load them in the order specified, but # append the training reactions unless the user specifically declares it not # to be included with a '!training' flag if '!training' not in depository_labels: @@ -690,7 +691,7 @@ def load_recipe(self, actions): for action in actions: action[0] = action[0].upper() valid_actions = [ - 'CHANGE_BOND', 'FORM_BOND', 'BREAK_BOND', 'GAIN_RADICAL', 'LOSE_RADICAL', + 'CHANGE_BOND', 'FORM_BOND', 'BREAK_BOND', 'GAIN_RADICAL', 'LOSE_RADICAL', 'GAIN_CHARGE', 'LOSE_CHARGE', 'GAIN_PAIR', 'LOSE_PAIR' ] if action[0] not in valid_actions: @@ -718,12 +719,12 @@ def save_training_reactions(self, reactions, reference=None, reference_type='', rank=3): """ This function takes a list of reactions appends it to the training reactions file. It ignores the existence of - duplicate reactions. - - The rank for each new reaction's kinetics is set to a default value of 3 unless the user specifies differently + duplicate reactions. + + The rank for each new reaction's kinetics is set to a default value of 3 unless the user specifies differently for those reactions. - - For each entry, the long description is imported from the kinetics comment. + + For each entry, the long description is imported from the kinetics comment. """ if not isinstance(reference, list): @@ -826,7 +827,7 @@ def save_training_reactions(self, reactions, reference=None, reference_type='', def save(self, path): """ - Save the current database to the file at location `path` on disk. + Save the current database to the file at location `path` on disk. """ self.save_groups(os.path.join(path, 'groups.py')) self.rules.save(os.path.join(path, 'rules.py')) @@ -843,7 +844,7 @@ def save_depository(self, depository, path): def save_groups(self, path): """ - Save the current database to the file at location `path` on disk. + Save the current database to the file at location `path` on disk. """ entries = self.groups.get_entries_to_save() @@ -879,7 +880,7 @@ def save_groups(self, path): f.write('reactantNum = {0}\n\n'.format(self.reactant_num)) if self.product_num is not None: f.write('productNum = {0}\n\n'.format(self.product_num)) - + if self.auto_generated is not None: f.write('autoGenerated = {0}\n\n'.format(self.auto_generated)) @@ -1009,14 +1010,14 @@ def generate_product_template(self, reactants0): def has_rate_rule(self, template): """ - Return ``True`` if a rate rule with the given `template` currently + Return ``True`` if a rate rule with the given `template` currently exists, or ``False`` otherwise. """ return self.rules.has_rule(template) def get_rate_rule(self, template): """ - Return the rate rule with the given `template`. Raises a + Return the rate rule with the given `template`. Raises a :class:`ValueError` if no corresponding entry exists. """ entry = self.rules.get_rule(template) @@ -1226,7 +1227,7 @@ def add_rules_from_training(self, thermo_database=None, train_indices=None): def get_root_template(self): """ Return the root template for the reaction family. Most of the time this - is the top-level nodes of the tree (as stored in the + is the top-level nodes of the tree (as stored in the :class:`KineticsGroups` object), but there are a few exceptions (e.g. R_Recombination). """ @@ -1391,22 +1392,23 @@ def apply_recipe(self, reactant_structures, forward=True, unique=True, relabel_a product_num = self.product_num or len(template.products) # Split product structure into multiple species if necessary - if (isinstance(product_structure, Group) and self.auto_generated and self.label in ["Intra_R_Add_Endocyclic","Intra_R_Add_Exocyclic"]): + if self.auto_generated and isinstance(reactant_structures[0],Group) and self.product_num == 1: product_structures = [product_structure] else: product_structures = product_structure.split() - # Make sure we've made the expected number of products - if product_num != len(product_structures): - # We have a different number of products than expected by the template. - # By definition this means that the template is not a match, so - # we return None to indicate that we could not generate the product - # structures - # We need to think this way in order to distinguish between - # intermolecular and intramolecular versions of reaction families, - # which will have very different kinetics - # Unfortunately this may also squash actual errors with malformed - # reaction templates - return None + + # Make sure we've made the expected number of products + if product_num != len(product_structures): + # We have a different number of products than expected by the template. + # By definition this means that the template is not a match, so + # we return None to indicate that we could not generate the product + # structures + # We need to think this way in order to distinguish between + # intermolecular and intramolecular versions of reaction families, + # which will have very different kinetics + # Unfortunately this may also squash actual errors with malformed + # reaction templates + return None # Remove vdW bonds for struct in product_structures: @@ -1445,17 +1447,17 @@ def apply_recipe(self, reactant_structures, forward=True, unique=True, relabel_a else: raise TypeError('Expecting Molecule or Group object, not {0}'.format(struct.__class__.__name__)) product_net_charge += struct.get_net_charge() - + if self.electrons < 0: if forward: reactant_net_charge += self.electrons - else: + else: product_net_charge += self.electrons elif self.electrons > 0: if forward: product_net_charge -= self.electrons - else: + else: reactant_net_charge -= self.electrons if reactant_net_charge != product_net_charge and is_molecule: @@ -1573,10 +1575,10 @@ def _generate_product_structures(self, reactant_structures, maps, forward, relab def is_molecule_forbidden(self, molecule): """ Return ``True`` if the molecule is forbidden in this family, or - ``False`` otherwise. + ``False`` otherwise. """ - # check family-specific forbidden structures + # check family-specific forbidden structures if self.forbidden is not None and self.forbidden.is_molecule_forbidden(molecule): return True @@ -1629,7 +1631,7 @@ def _create_reaction(self, reactants, products, is_forward): def _match_reactant_to_template(self, reactant, template_reactant): """ - Return a complete list of the mappings if the provided reactant + Return a complete list of the mappings if the provided reactant matches the provided template reactant, or an empty list if not. """ @@ -1805,8 +1807,10 @@ def calculate_degeneracy(self, reaction, resonance=True): For a `reaction` with `Molecule` or `Species` objects given in the direction in which the kinetics are defined, compute the reaction-path degeneracy. Can specify whether to consider resonance. - This method by default adjusts for double counting of identical reactants. - This should only be adjusted once per reaction. + This method by default adjusts for double counting of identical reactants. + This should only be adjusted once per reaction. To not adjust for + identical reactants (since you will be reducing them later in the algorithm), add + `ignoreSameReactants= True` to this method. """ # Check if the reactants are the same # If they refer to the same memory address, then make a deep copy so @@ -1875,7 +1879,7 @@ def _generate_reactions(self, reactants, products=None, forward=True, prod_reson rxn_list = [] - # Wrap each reactant in a list if not already done (this is done to + # Wrap each reactant in a list if not already done (this is done to # allow for passing multiple resonance structures for each molecule) # This also makes a copy of the reactants list so we don't modify the # original @@ -2189,7 +2193,7 @@ def generate_products_and_reactions(order): if not forward and ('adsorption' in self.label.lower() or 'eleyrideal' in self.label.lower()): # Desorption should have desorbed something (else it was probably bidentate) # so delete reactions that don't make a gas-phase desorbed product - # Eley-Rideal reactions should have one gas-phase product in the reverse direction + # Eley-Rideal reactions should have one gas-phase product in the reverse direction # Determine how many surf reactants we expect based on the template n_surf_expected = len([r for r in self.forward_template.reactants if r.item.contains_surface_site()]) @@ -2259,7 +2263,7 @@ def get_reaction_pairs(self, reaction): """ pairs = [] if len(reaction.reactants) == 1 or len(reaction.products) == 1: - # When there is only one reactant (or one product), it is paired + # When there is only one reactant (or one product), it is paired # with each of the products (reactants) for reactant in reaction.reactants: for product in reaction.products: @@ -2443,7 +2447,7 @@ def get_kinetics_for_template(self, template, degeneracy=1, method='rate rules') def get_kinetics_from_depository(self, depository, reaction, template, degeneracy): """ Search the given `depository` in this kinetics family for kinetics - for the given `reaction`. Returns a list of all of the matching + for the given `reaction`. Returns a list of all of the matching kinetics, the corresponding entries, and ``True`` if the kinetics match the forward direction or ``False`` if they match the reverse direction. @@ -2545,11 +2549,35 @@ def get_kinetics(self, reaction, template_labels, degeneracy=1, estimator='', re return kinetics_list + def estimate_kinetics_using_group_additivity(self, template, degeneracy=1): + """ + Determine the appropriate kinetics for a reaction with the given + `template` using group additivity. + + Returns just the kinetics, or None. + """ + warnings.warn("Group additivity is no longer supported and may be" + " removed in version 2.3.", DeprecationWarning) + # Start with the generic kinetics of the top-level nodes + kinetics = None + root = self.get_root_template() + kinetics = self.get_kinetics_for_template(root) + + if kinetics is None: + # raise UndeterminableKineticsError('Cannot determine group additivity kinetics estimate for ' + # 'template "{0}".'.format(','.join([e.label for e in template]))) + return None + else: + kinetics = kinetics[0] + + # Now add in more specific corrections if possible + return self.groups.estimate_kinetics_using_group_additivity(template, kinetics, degeneracy) + def estimate_kinetics_using_rate_rules(self, template, degeneracy=1): """ Determine the appropriate kinetics for a reaction with the given `template` using rate rules. - + Returns a tuple (kinetics, entry) where `entry` is the database entry used to determine the kinetics only if it is an exact match, and is None if some averaging or use of a parent node took place. @@ -2560,8 +2588,8 @@ def estimate_kinetics_using_rate_rules(self, template, degeneracy=1): def get_reaction_template_labels(self, reaction): """ - Retrieve the template for the reaction and - return the corresponding labels for each of the + Retrieve the template for the reaction and + return the corresponding labels for each of the groups in the template. """ template = self.get_reaction_template(reaction) @@ -2574,8 +2602,8 @@ def get_reaction_template_labels(self, reaction): def retrieve_template(self, template_labels): """ - Reconstruct the groups associated with the - labels of the reaction template and + Reconstruct the groups associated with the + labels of the reaction template and return a list. """ template = [] @@ -2586,9 +2614,9 @@ def retrieve_template(self, template_labels): def get_labeled_reactants_and_products(self, reactants, products, relabel_atoms=True): """ - Given `reactants`, a list of :class:`Molecule` objects, and products, a list of - :class:`Molecule` objects, return two new lists of :class:`Molecule` objects with - atoms labeled: one for reactants, one for products. Returned molecules are totally + Given `reactants`, a list of :class:`Molecule` objects, and products, a list of + :class:`Molecule` objects, return two new lists of :class:`Molecule` objects with + atoms labeled: one for reactants, one for products. Returned molecules are totally new entities in memory so input molecules `reactants` and `products` won't be affected. If RMG cannot find appropriate labels, (None, None) will be returned. If ``relabel_atoms`` is ``True``, product atom labels of reversible families @@ -2767,7 +2795,7 @@ def add_entry(self, parent, grp, name): def _split_reactions(self, rxns, newgrp): """ divides the reactions in rxns between the new - group structure newgrp and the old structure with + group structure newgrp and the old structure with label oldlabel returns a list of reactions associated with the new group the list of reactions associated with the old group @@ -2792,14 +2820,14 @@ def _split_reactions(self, rxns, newgrp): comp.append(rxn) return new, comp, new_inds - + def reaction_matches(self, rxn, grp): rmol = rxn.reactants[0].molecule[0] for r in rxn.reactants[1:]: rmol = rmol.merge(r.molecule[0]) rmol.identify_ring_membership() return rmol.is_subgraph_isomorphic(grp, generate_initial_map=True, save_order=True) - + def eval_ext(self, parent, ext, extname, template_rxn_map, obj=None, T=1000.0): """ evaluates the objective function obj @@ -2823,15 +2851,15 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, finds the set of all extension groups to parent such that 1) the extension group divides the set of reactions under parent 2) No generalization of the extension group divides the set of reactions under parent - + We find this by generating all possible extensions of the initial group. Extensions that split reactions are added - to the list. All extensions that do not split reactions and do not create bonds are ignored + to the list. All extensions that do not split reactions and do not create bonds are ignored (although those that match every reaction are labeled so we don't search them twice). Those that match - all reactions and involve bond creation undergo this process again. - - Principle: Say you have two elementary changes to a group ext1 and ext2 if applying ext1 and ext2 results in a + all reactions and involve bond creation undergo this process again. + + Principle: Say you have two elementary changes to a group ext1 and ext2 if applying ext1 and ext2 results in a split at least one of ext1 and ext2 must result in a split - + Speed of this algorithm relies heavily on searching non bond creation dimensions once. """ out_exts = [[]] @@ -2842,7 +2870,7 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, n_splits = len(template_rxn_map[parent.label][0].reactants) iter = 0 - + while grps[iter] != []: grp = grps[iter][-1] @@ -2961,7 +2989,7 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, out_exts.append([]) grps[iter].pop() names.pop() - + for ind in ext_inds: # collect the groups to be expanded grpr, grpcr, namer, typr, indcr = exts[ind] if len(grps) == iter+1: @@ -2971,17 +2999,17 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, if first_time: first_time = False - + if grps[iter] == [] and len(grps) != iter+1 and (not (any([len(x)>0 for x in out_exts]) and iter+1 > iter_max)): iter += 1 if len(grps[iter]) > iter_item_cap: logging.error("Recursion item cap hit not splitting {0} reactions at iter {1} with {2} items".format(len(template_rxn_map[parent.label]),iter,len(grps[iter]))) iter -= 1 gave_up_split = True - + elif grps[iter] == [] and len(grps) != iter+1 and (any([len(x)>0 for x in out_exts]) and iter+1 > iter_max): logging.error("iter_max achieved terminating early") - + out = [] # compile all of the valid extensions together # may be some duplicates here, but I don't think it's currently worth identifying them @@ -2992,7 +3020,7 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, def extend_node(self, parent, template_rxn_map, obj=None, T=1000.0, iter_max=np.inf, iter_item_cap=np.inf): """ - Constructs an extension to the group parent based on evaluation + Constructs an extension to the group parent based on evaluation of the objective function obj """ exts, gave_up_split = self.get_extension_edge(parent, template_rxn_map, obj=obj, T=T, iter_max=iter_max, iter_item_cap=iter_item_cap) @@ -3048,10 +3076,10 @@ def extend_node(self, parent, template_rxn_map, obj=None, T=1000.0, iter_max=np. parent.item.clear_reg_dims() # this almost always solves the problem return True return False - + if gave_up_split: return False - + vals = [] for grp, grpc, name, typ, einds in exts: val, boo = self.eval_ext(parent, grp, name, template_rxn_map, obj, T) @@ -3147,7 +3175,7 @@ def extend_node(self, parent, template_rxn_map, obj=None, T=1000.0, iter_max=np. logging.error(prod.label) logging.error(prod.to_adjacency_list()) raise ValueError - + template_rxn_map[extname] = new_entries if complement: @@ -3163,21 +3191,21 @@ def generate_tree(self, rxns=None, obj=None, thermo_database=None, T=1000.0, npr """ Generate a tree by greedy optimization based on the objective function obj the optimization is done by iterating through every group and if the group has - more than one training reaction associated with it a set of potential more specific extensions - are generated and the extension that optimizing the objective function combination is chosen + more than one training reaction associated with it a set of potential more specific extensions + are generated and the extension that optimizing the objective function combination is chosen and the iteration starts over at the beginning - + additionally the tree structure is simplified on the fly by removing groups that have no kinetics data associated if their parent has no kinetics data associated and they either have only one child or have two children one of which has no kinetics data and no children (its parent becomes the parent of its only relevant child node) - + Args: rxns: List of reactions to generate tree from (if None pull the whole training set) obj: Object to expand tree from (if None uses top node) thermo_database: Thermodynamic database used for reversing training reactions T: Temperature the tree is optimized for - nprocs: Number of process for parallel tree generation + nprocs: Number of process for parallel tree generation min_splitable_entry_num: the minimum number of splitable reactions at a node in order to spawn a new process solving that node min_rxns_to_spawn: the minimum number of reactions at a node to spawn a new process solving that node @@ -3221,9 +3249,9 @@ def rxnkey(rxn): min_splitable_entry_num=min_splitable_entry_num, min_rxns_to_spawn=min_rxns_to_spawn, extension_iter_max=extension_iter_max, extension_iter_item_cap=extension_iter_item_cap) logging.error("built tree with {} nodes".format(len(list(self.groups.entries)))) - + self.auto_generated = True - + def get_rxn_batches(self, rxns, T=1000.0, max_batch_size=800, outlier_fraction=0.02, stratum_num=8): """ Breaks reactions into batches based on a modified stratified sampling scheme @@ -3326,7 +3354,7 @@ def make_tree_nodes(self, template_rxn_map=None, obj=None, T=1000.0, nprocs=0, d entries.remove(entry) else: psize = float(len(template_rxn_map[root.label])) - + logging.error(psize) mult_completed_nodes = [] # nodes containing multiple identical training reactions boo = True # if the for loop doesn't break becomes false and the while loop terminates @@ -3486,7 +3514,7 @@ def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1 """ Perform K-fold cross validation on an automatically generated tree at temperature T after finding an appropriate node for kinetics estimation it will move up the tree - iters times. + iters times. Returns a dictionary mapping {rxn:Ln(k_Est/k_Train)} """ @@ -3539,13 +3567,17 @@ def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1 entry = entry.parent uncertainties[rxn] = self.rules.entries[entry.label][0].data.uncertainty - + if not ascend: L = list(set(template_rxn_map[entry.label]) - set(rxns_test)) if L != []: + if isinstance(L[0].kinetics,Arrhenius): kinetics = ArrheniusBM().fit_to_reactions(L, recipe=self.forward_recipe.actions) kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(T)) + else: + kinetics = ArrheniusChargeTransferBM().fit_to_reactions(L, recipe=self.forward_recipe.actions) + kinetics = kinetics.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(T)) k = kinetics.get_rate_coefficient(T) errors[rxn] = np.log(k / krxn) else: @@ -3557,7 +3589,7 @@ def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1 logging.error("determining fold rate") c = 1 while boo: - parent = entry.parent + parent = entry.parent if parent is None: break rlistparent = list(set(template_rxn_map[parent.label]) - set(rxns_test)) @@ -3571,11 +3603,11 @@ def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1 c += 1 else: boo = False - + kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(T)) k = kinetics.get_rate_coefficient(T) errors[rxn] = np.log(k / krxn) - + return errors, uncertainties def cross_validate_old(self, folds=5, T=1000.0, random_state=1, estimator='rate rules', thermo_database=None, get_reverse=False, uncertainties=True): @@ -3585,7 +3617,7 @@ def cross_validate_old(self, folds=5, T=1000.0, random_state=1, estimator='rate """ errors = {} uncs = {} - + kpu = KineticParameterUncertainty() rxns = np.array(self.get_training_set(remove_degeneracy=True,get_reverse=get_reverse)) @@ -3629,7 +3661,7 @@ def cross_validate_old(self, folds=5, T=1000.0, random_state=1, estimator='rate boo,source = self.extract_source_from_comments(testrxn) sdict = {"Rate Rules":source} uncs[rxn] = kpu.get_uncertainty_value(sdict) - + if uncertainties: return errors, uncs else: @@ -3639,19 +3671,19 @@ def simple_regularization(self, node, template_rxn_map, test=True): """ Simplest regularization algorithm All nodes are made as specific as their descendant reactions - Training reactions are assumed to not generalize + Training reactions are assumed to not generalize For example if an particular atom at a node is Oxygen for all of its descendent reactions a reaction where it is Sulfur will never hit that node - unless it is the top node even if the tree did not split on the identity + unless it is the top node even if the tree did not split on the identity of that atom - - The test option to this function determines whether or not the reactions - under a node match the extended group before adding an extension. - If the test fails the extension is skipped. - - In general test=True is needed if the cascade algorithm was used + + The test option to this function determines whether or not the reactions + under a node match the extended group before adding an extension. + If the test fails the extension is skipped. + + In general test=True is needed if the cascade algorithm was used to generate the tree and test=False is ok if the cascade algorithm - wasn't used. + wasn't used. """ for child in node.children: @@ -3857,7 +3889,7 @@ def clean_tree(self): def save_generated_tree(self, path=None): """ - clears the rules and saves the family to its + clears the rules and saves the family to its current location in database """ if path is None: @@ -3871,7 +3903,7 @@ def get_training_set(self, thermo_database=None, remove_degeneracy=False, estima """ retrieves all reactions in the training set, assigns thermo to the species objects reverses reactions as necessary so that all reactions are in the forward direction - and returns the resulting list of reactions in the forward direction with thermo + and returns the resulting list of reactions in the forward direction with thermo assigned """ @@ -3941,7 +3973,7 @@ def get_reactant_thermo(reactant,metal): root_labels = [x.label for x in root.atoms if x.label != ''] root_label_set = set(root_labels) - + for i, entry in enumerate(entries): if estimate_thermo: # parse out the metal to scale to @@ -3971,6 +4003,8 @@ def get_reactant_thermo(reactant,metal): else: mol = deepcopy(react.molecule[0]) + mol.update_atomtypes() + if fix_labels: for prod in rxns[i].products: fix_labels_mol(prod.molecule[0], root_labels) @@ -3989,12 +4023,12 @@ def get_reactant_thermo(reactant,metal): mol = mol.merge(react.molecule[0]) else: mol = deepcopy(react.molecule[0]) - + if fix_labels: mol_label_set = set([x.label for x in get_label_fixed_mol(mol, root_labels).atoms if x.label != '']) else: mol_label_set = set([x.label for x in mol.atoms if x.label != '']) - + if mol_label_set == root_label_set and ((mol.is_subgraph_isomorphic(root, generate_initial_map=True) or (not fix_labels and get_label_fixed_mol(mol, root_labels).is_subgraph_isomorphic(root, generate_initial_map=True)))): @@ -4056,6 +4090,8 @@ def get_reactant_thermo(reactant,metal): else: mol = deepcopy(react.molecule[0]) + mol.update_atomtypes() + if (mol.is_subgraph_isomorphic(root, generate_initial_map=True) or (not fix_labels and get_label_fixed_mol(mol, root_labels).is_subgraph_isomorphic(root, generate_initial_map=True))): # try product structures @@ -4084,7 +4120,7 @@ def get_reactant_thermo(reactant,metal): def get_reaction_matches(self, rxns=None, thermo_database=None, remove_degeneracy=False, estimate_thermo=True, fix_labels=False, exact_matches_only=False, get_reverse=False): """ - returns a dictionary mapping for each entry in the tree: + returns a dictionary mapping for each entry in the tree: (entry.label,entry.item) : list of all training reactions (or the list given) that match that entry """ if rxns is None: @@ -4177,10 +4213,10 @@ def retrieve_original_entry(self, template_label): """ Retrieves the original entry, be it a rule or training reaction, given the template label in the form 'group1;group2' or 'group1;group2;group3' - + Returns tuple in the form (RateRuleEntry, TrainingReactionEntry) - + Where the TrainingReactionEntry is only present if it comes from a training reaction """ template_labels = template_label.split()[-1].split(';') @@ -4197,13 +4233,13 @@ def get_sources_for_template(self, template): """ Returns the set of rate rules and training reactions used to average this `template`. Note that the tree must be averaged with verbose=True for this to work. - + Returns a tuple of rules, training - - where rules are a list of tuples containing + + where rules are a list of tuples containing the [(original_entry, weight_used_in_average), ... ] - + and training is a list of tuples containing the [(rate_rule_entry, training_reaction_entry, weight_used_in_average),...] """ @@ -4211,7 +4247,7 @@ def get_sources_for_template(self, template): def assign_weights_to_entries(entry_nested_list, weighted_entries, n=1): """ Assign weights to an average of average nested list. Where n is the - number of values being averaged recursively. + number of values being averaged recursively. """ n = len(entry_nested_list) * n for entry in entry_nested_list: @@ -4285,7 +4321,7 @@ def assign_weights_to_entries(entry_nested_list, weighted_entries, n=1): rules[rule_entry] += weight else: rules[rule_entry] = weight - # Each entry should now only appear once + # Each entry should now only appear once training = [(k[0], k[1], v) for k, v in training.items()] rules = list(rules.items()) @@ -4296,11 +4332,11 @@ def extract_source_from_comments(self, reaction): Returns the rate rule associated with the kinetics of a reaction by parsing the comments. Will return the template associated with the matched rate rule. Returns a tuple containing (Boolean_Is_Kinetics_From_Training_reaction, Source_Data) - + For a training reaction, the Source_Data returns:: [Family_Label, Training_Reaction_Entry, Kinetics_In_Reverse?] - + For a reaction from rate rules, the Source_Data is a tuple containing:: [Family_Label, {'template': originalTemplate, @@ -4335,7 +4371,7 @@ def extract_source_from_comments(self, reaction): 'but does not match the training reaction {1} from the ' '{2} family.'.format(reaction, training_reaction_index, self.label)) - # Sometimes the matched kinetics could be in the reverse direction..... + # Sometimes the matched kinetics could be in the reverse direction..... if reaction.is_isomorphic(training_entry.item, either_direction=False, save_order=self.save_order): reverse = False else: @@ -4349,7 +4385,7 @@ def extract_source_from_comments(self, reaction): elif line.startswith('Multiplied by'): degeneracy = float(line.split()[-1]) - # Extract the rate rule information + # Extract the rate rule information full_comment_string = reaction.kinetics.comment.replace('\n', ' ') # The rate rule string is right after the phrase 'for rate rule' @@ -4498,7 +4534,7 @@ def _child_make_tree_nodes(family, child_conn, template_rxn_map, obj, T, nprocs, family.groups.entries[root_label].parent = None family.make_tree_nodes(template_rxn_map=template_rxn_map, obj=obj, T=T, nprocs=nprocs, depth=depth + 1, - min_splitable_entry_num=min_splitable_entry_num, min_rxns_to_spawn=min_rxns_to_spawn, + min_splitable_entry_num=min_splitable_entry_num, min_rxns_to_spawn=min_rxns_to_spawn, extension_iter_max=extension_iter_max, extension_iter_item_cap=extension_iter_item_cap) child_conn.send(list(family.groups.entries.values())) From b207876ce118b836994734892e8fddfa6a522e18 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 14:45:12 -0500 Subject: [PATCH 040/109] enable surface and bulk potentials to be specified separately --- rmgpy/rmg/input.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rmgpy/rmg/input.py b/rmgpy/rmg/input.py index c94a7f58e1..8219fcb66c 100644 --- a/rmgpy/rmg/input.py +++ b/rmgpy/rmg/input.py @@ -633,7 +633,8 @@ def liquid_cat_reactor(temperature, initialConcentrations, initialSurfaceCoverages, surfaceVolumeRatio, - potential=None, + surfPotential=None, + liqPotential=None, terminationConversion=None, terminationTime=None, terminationRateRatio=None, @@ -709,8 +710,10 @@ def liquid_cat_reactor(temperature, initialCondSurf[key] = item*rmg.surface_site_density.value_si*A initialCondSurf["T"] = T initialCondSurf["A"] = A - if potential: - initialCondSurf["phi"] = Quantity(potential).value_si + if surfPotential: + initialCondSurf["Phi"] = Quantity(surfPotential).value_si + if liqPotential: + initialCondLiq["Phi"] = Quantity(liqPotential).value_si system = ConstantTLiquidSurfaceReactor(rmg.reaction_model.core.phase_system, rmg.reaction_model.edge.phase_system, {"liquid":initialCondLiq,"surface":initialCondSurf},termination,constantSpecies) From c9343c2dc3c04b2d1cd21452b4236d960b959cd7 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 13:26:32 -0500 Subject: [PATCH 041/109] avoid issue with auto decay reactions --- rmgpy/chemkin.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rmgpy/chemkin.pyx b/rmgpy/chemkin.pyx index d28ee47ce0..068410dacf 100644 --- a/rmgpy/chemkin.pyx +++ b/rmgpy/chemkin.pyx @@ -758,7 +758,7 @@ def read_reaction_comments(reaction, comments, read=True): raise ChemkinError('Unexpected species identifier {0} encountered in flux pairs ' 'for reaction {1}.'.format(prod_str, reaction)) reaction.pairs.append((reactant, product)) - assert len(reaction.pairs) == max(len(reaction.reactants), len(reaction.products)) + #assert len(reaction.pairs) == max(len(reaction.reactants), len(reaction.products)) elif isinstance(reaction, TemplateReaction) and 'rate rule ' in line: bracketed_rule = tokens[-1] From eaf8fc6bd593dfac99dce7ee1b436abd45afee6b Mon Sep 17 00:00:00 2001 From: Richard West Date: Mon, 17 Jul 2023 12:19:20 -0400 Subject: [PATCH 042/109] Added Charge Transfer types to average_kinetics --- rmgpy/data/kinetics/family.py | 37 ++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 2ec538e8ec..ed6f5c9b5a 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4548,6 +4548,16 @@ def average_kinetics(kinetics_list): logA = 0.0 n = 0.0 Ea = 0.0 + alpha = 0.5 + electrons = None + if isinstance(kinetics_list[0], (SurfaceChargeTransfer, ArrheniusChargeTransfer)): + if electrons is None: + electrons = kinetics_list[0].electrons.value_si + if not all(np.abs(k.V0.value_si) < 0.0001 for k in kinetics_list): + raise ValueError(f"Trying to average charge transfer rates with non-zero V0 values: {[k.V0.value_si for k in kinetics_list]}") + if not all(np.abs(k.alpha.value_si - 0.5) < 0.001 for k in kinetics_list): + raise ValueError(f"Trying to average charge transfer rates with alpha values not equal to 0.5: {[k.alpha for k in kinetics_list]}") + V0 = 0.0 count = 0 for kinetics in kinetics_list: count += 1 @@ -4557,6 +4567,7 @@ def average_kinetics(kinetics_list): logA /= count n /= count + alpha /= count Ea /= count ## The above could be replaced with something like: @@ -4582,13 +4593,29 @@ def average_kinetics(kinetics_list): # surface: sticking coefficient pass else: - raise Exception(f'Invalid units {Aunits} for averaging kinetics.') + raise ValueError(f'Invalid units {Aunits} for averaging kinetics.') - if type(kinetics) not in [Arrhenius,]: - raise Exception(f'Invalid kinetics type {type(kinetics)!r} for {self!r}.') + if type(kinetics) not in {Arrhenius, SurfaceChargeTransfer, ArrheniusChargeTransfer}: + raise TypeError(f'Invalid kinetics type {type(kinetics)!r} for {self!r}.') - if False: - pass + if isinstance(kinetics, SurfaceChargeTransfer): + averaged_kinetics = SurfaceChargeTransfer( + A=(10 ** logA, Aunits), + n=n, + electrons=electrons, + alpha=alpha, + V0=(V0,'V'), + Ea=(Ea * 0.001, "kJ/mol"), + ) + elif isinstance(kinetics, ArrheniusChargeTransfer): + averaged_kinetics = ArrheniusChargeTransfer( + A=(10 ** logA, Aunits), + n=n, + electrons=electrons, + alpha=alpha, + V0=(V0,'V'), + Ea=(Ea * 0.001, "kJ/mol"), + ) else: averaged_kinetics = Arrhenius( A=(10 ** logA, Aunits), From 226af17914369171a12f9a5e7f3dd75ff73b0d9a Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 14:32:50 -0500 Subject: [PATCH 043/109] update rule fitting 1) Enable fitting of ArrheniusChargeTransferBM 2) if the BM fit is bad: E0 < 0, A > 1e30 or abs(n) > 5 just average the kinetics --- rmgpy/data/kinetics/family.py | 46 ++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index ed6f5c9b5a..b8bfbb6143 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4487,17 +4487,51 @@ def _make_rule(rr): rxns = np.array(rxns) data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rxns])) if n > 0: - kin = ArrheniusBM().fit_to_reactions(rxns, recipe=recipe) - if n == 1: - kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + if isinstance(rxns[0].kinetics, Arrhenius): + arr = ArrheniusBM else: - dlnks = np.array([ - np.log( - ArrheniusBM().fit_to_reactions(rxns[list(set(range(len(rxns))) - {i})], recipe=recipe) + arr = ArrheniusChargeTransferBM + kin = arr().fit_to_reactions(rxns, recipe=recipe) + if kin.E0.value_si < 0.0 or kin.A.value_si > 1.0e30 or abs(kin.n.value_si) > 5.0: + kin = average_kinetics([r.kinetics for r in rxns]) + kin.comment = "E0<0 in the Arrhenius BM fit. Instead averaged from {} reactions.".format(n) + if n == 1: + kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + else: + dlnks = np.array([ + np.log( + average_kinetics([r.kinetics for r in rxns[list(set(range(len(rxns))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rxns) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + return kin + else: + if n == 1: + kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + else: + if isinstance(rxns[0].kinetics, Arrhenius): + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rxns[list(set(range(len(rxns))) - {i})], recipe=recipe) .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) ) for i, rxn in enumerate(rxns) ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + else: + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rxns[list(set(range(len(rxns))) - {i})], recipe=recipe) + .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rxns) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 # weighted average calculations ws = 1.0 / varis From c870e529307b5d88dd5cfdb43707bdfd4f1c81db Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Mon, 28 Feb 2022 11:41:14 -0500 Subject: [PATCH 044/109] Allow approximating a solvent not in the database with another solvent --- rmgpy/data/solvation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index ac162ea780..c9746090af 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -2154,7 +2154,7 @@ def check_solvent_in_initial_species(self, rmg, solvent_structure): if not any([spec.is_solvent for spec in rmg.initial_species]): if solvent_structure is not None: logging.info('One of the initial species must be the solvent') - raise ValueError('One of the initial species must be the solvent') + logging.warning("Solvent is not an initial species") else: logging.info('One of the initial species must be the solvent with the same string name') - raise ValueError('One of the initial species must be the solvent with the same string name') + logging.warning("Solvent is not an initial species with the same string name") From 4374232e00b950c86338c48d9c87ac0417c41425 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Tue, 11 Jan 2022 18:30:18 -0500 Subject: [PATCH 045/109] standardize ascend option in cross validate ascend based on original uncertainties and compute errors at that node in cross validate --- rmgpy/data/kinetics/family.py | 60 ++++++++++++++++------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index b8bfbb6143..e9539c504a 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -3510,7 +3510,7 @@ def make_bm_rules_from_template_rxn_map(self, template_rxn_map, nprocs=1, Tref=1 index += 1 - def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1000.0, iters=0, random_state=1, ascend=False): + def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1000.0, iters=0, random_state=1): """ Perform K-fold cross validation on an automatically generated tree at temperature T after finding an appropriate node for kinetics estimation it will move up the tree @@ -3566,47 +3566,43 @@ def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1 if entry.parent: entry = entry.parent + boo = True + + while boo: + if entry.parent is None: + break + kin = self.rules.entries[entry.label][0].data + kinparent = self.rules.entries[entry.parent.label][0].data + err_parent = abs(kinparent.uncertainty.data_mean + kinparent.uncertainty.mu - kin.uncertainty.data_mean) + np.sqrt(2.0*kinparent.uncertainty.var/np.pi) + err_entry = abs(kin.uncertainty.mu) + np.sqrt(2.0*kin.uncertainty.var/np.pi) + if err_entry <= err_parent: + break + else: + entry = entry.parent + uncertainties[rxn] = self.rules.entries[entry.label][0].data.uncertainty - if not ascend: - L = list(set(template_rxn_map[entry.label]) - set(rxns_test)) - if L != []: - if isinstance(L[0].kinetics,Arrhenius): + L = list(set(template_rxn_map[entry.label]) - set(rxns_test)) + + if L != []: + if isinstance(L[0].kinetics, Arrhenius): kinetics = ArrheniusBM().fit_to_reactions(L, recipe=self.forward_recipe.actions) - kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(T)) + if kinetics.E0.value_si < 0.0 or len(L) == 1: + kinetics = average_kinetics([r.kinetics for r in L]) else: - kinetics = ArrheniusChargeTransferBM().fit_to_reactions(L, recipe=self.forward_recipe.actions) - kinetics = kinetics.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(T)) - k = kinetics.get_rate_coefficient(T) - errors[rxn] = np.log(k / krxn) + kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(298.0)) else: - raise ValueError('only one piece of kinetics information in the tree?') - else: - boo = True - rlist = list(set(template_rxn_map[entry.label]) - set(rxns_test)) - kinetics = _make_rule((self.forward_recipe.actions,rlist,T,1.0e3,"",[rxn.rank for rxn in rlist])) - logging.error("determining fold rate") - c = 1 - while boo: - parent = entry.parent - if parent is None: - break - rlistparent = list(set(template_rxn_map[parent.label]) - set(rxns_test)) - kineticsparent = _make_rule((self.forward_recipe.actions,rlistparent,T,1.0e3,"",[rxn.rank for rxn in rlistparent])) - err_parent = abs(kineticsparent.uncertainty.data_mean + kineticsparent.uncertainty.mu - kinetics.uncertainty.data_mean) + np.sqrt(2.0*kineticsparent.uncertainty.var/np.pi) - err_entry = abs(kinetics.uncertainty.mu) + np.sqrt(2.0*kinetics.uncertainty.var/np.pi) - if err_entry > err_parent: - entry = entry.parent - kinetics = kineticsparent - logging.error("recursing {}".format(c)) - c += 1 + kinetics = ArrheniusChargeTransferBM().fit_to_reactions(L, recipe=self.forward_recipe.actions) + if kinetics.E0.value_si < 0.0 or len(L) == 1: + kinetics = average_kinetics([r.kinetics for r in L]) else: - boo = False + kinetics = kinetics.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(298.0)) - kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(T)) k = kinetics.get_rate_coefficient(T) errors[rxn] = np.log(k / krxn) + else: + raise ValueError('only one piece of kinetics information in the tree?') return errors, uncertainties From d0126887698c3ea04d2391e3389991d1b381af54 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Mon, 28 Feb 2022 12:03:57 -0500 Subject: [PATCH 046/109] Average kinetics when n=1 or E0<0* add comment to averaged kinetics *(Commit updated by rwest during rebase: In the original commit the comment used to specify /why/ it was averaged, but it is added in the averaging method itself. The /why/, if needed, should be added elsewhere.) --- rmgpy/data/kinetics/family.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index e9539c504a..1db06383d8 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4487,10 +4487,11 @@ def _make_rule(rr): arr = ArrheniusBM else: arr = ArrheniusChargeTransferBM - kin = arr().fit_to_reactions(rxns, recipe=recipe) - if kin.E0.value_si < 0.0 or kin.A.value_si > 1.0e30 or abs(kin.n.value_si) > 5.0: + if n > 1: + kin = arr().fit_to_reactions(rxns, recipe=recipe) + if n == 1 or kin.E0.value_si < 0.0: kin = average_kinetics([r.kinetics for r in rxns]) - kin.comment = "E0<0 in the Arrhenius BM fit. Instead averaged from {} reactions.".format(n) + #kin.comment = "Only one reaction or Arrhenius BM fit bad. Instead averaged from {} reactions.".format(n) if n == 1: kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) else: @@ -4528,14 +4529,15 @@ def _make_rule(rr): .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) ) for i, rxn in enumerate(rxns) ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 - # weighted average calculations - ws = 1.0 / varis - V1 = ws.sum() - V2 = (ws ** 2).sum() - mu = np.dot(ws, dlnks) / V1 - s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) - kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) return kin else: return None @@ -4651,5 +4653,6 @@ def average_kinetics(kinetics_list): A=(10 ** logA, Aunits), n=n, Ea=(Ea * 0.001, "kJ/mol"), + comment=f"Averaged from {len(kinetics_list)} rate expressions.", ) return averaged_kinetics From 34631b4bc7e1b70adafe6bd4613a50a9f5965205 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Mon, 28 Feb 2022 12:14:04 -0500 Subject: [PATCH 047/109] fix BM fitting consistenty use dH298 do not estimate e0 = w0/10 when |dHrxn| > 0.4w0 --- rmgpy/kinetics/arrhenius.pyx | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/rmgpy/kinetics/arrhenius.pyx b/rmgpy/kinetics/arrhenius.pyx index db654c21ea..19e3221acc 100644 --- a/rmgpy/kinetics/arrhenius.pyx +++ b/rmgpy/kinetics/arrhenius.pyx @@ -617,27 +617,24 @@ cdef class ArrheniusBM(KineticsModel): if len(rxns) == 1: T = 1000.0 rxn = rxns[0] - dHrxn = rxn.get_enthalpy_of_reaction(T) + dHrxn = rxn.get_enthalpy_of_reaction(298.0) A = rxn.kinetics.A.value_si n = rxn.kinetics.n.value_si Ea = rxn.kinetics.Ea.value_si - + def kfcn(E0): Vp = 2 * w0 * (2 * w0 + 2 * E0) / (2 * w0 - 2 * E0) out = Ea - (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) * (Vp - 2 * w0 + dHrxn) / (Vp * Vp - (2 * w0) * (2 * w0) + dHrxn * dHrxn) return out - if abs(dHrxn) > 4 * w0 / 10.0: - E0 = w0 / 10.0 - else: - E0 = fsolve(kfcn, w0 / 10.0)[0] + E0 = fsolve(kfcn, w0 / 10.0)[0] self.Tmin = rxn.kinetics.Tmin self.Tmax = rxn.kinetics.Tmax self.solute = None self.comment = 'Fitted to {0} reaction at temperature: {1} K'.format(len(rxns), T) else: - # define optimization function + # define optimization function def kfcn(xs, lnA, n, E0): T = xs[:,0] dHrxn = xs[:,1] @@ -646,7 +643,7 @@ cdef class ArrheniusBM(KineticsModel): Ea = np.where(dHrxn< -4.0*E0, 0.0, Ea) Ea = np.where(dHrxn > 4.0*E0, dHrxn, Ea) return lnA + np.log(T ** n * np.exp(-Ea / (8.314 * T))) - + # get (T,dHrxn(T)) -> (Ln(k) mappings xdata = [] ydata = [] @@ -655,7 +652,7 @@ cdef class ArrheniusBM(KineticsModel): # approximately correct the overall uncertainties to std deviations s = rank_accuracy_map[rxn.rank].value_si/2.0 for T in Ts: - xdata.append([T, rxn.get_enthalpy_of_reaction(T)]) + xdata.append([T, rxn.get_enthalpy_of_reaction(298.0)]) ydata.append(np.log(rxn.get_rate_coefficient(T))) sigmas.append(s / (8.314 * T)) @@ -1554,9 +1551,8 @@ cdef class ArrheniusChargeTransferBM(KineticsModel): w0 = sum(w0s) / len(w0s) if len(rxns) == 1: - T = 1000.0 rxn = rxns[0] - dHrxn = rxn.get_enthalpy_of_reaction(T) + dHrxn = rxn.get_enthalpy_of_reaction(298.0) A = rxn.kinetics.A.value_si n = rxn.kinetics.n.value_si Ea = rxn.kinetics.Ea.value_si @@ -1566,14 +1562,11 @@ cdef class ArrheniusChargeTransferBM(KineticsModel): out = Ea - (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) * (Vp - 2 * w0 + dHrxn) / (Vp * Vp - (2 * w0) * (2 * w0) + dHrxn * dHrxn) return out - if abs(dHrxn) > 4 * w0 / 10.0: - E0 = w0 / 10.0 - else: - E0 = fsolve(kfcn, w0 / 10.0)[0] + E0 = fsolve(kfcn, w0 / 10.0)[0] self.Tmin = rxn.kinetics.Tmin self.Tmax = rxn.kinetics.Tmax - self.comment = 'Fitted to {0} reaction at temperature: {1} K'.format(len(rxns), T) + self.comment = 'Fitted to 1 reaction' else: # define optimization function def kfcn(xs, lnA, n, E0): @@ -1593,7 +1586,7 @@ cdef class ArrheniusChargeTransferBM(KineticsModel): # approximately correct the overall uncertainties to std deviations s = rank_accuracy_map[rxn.rank].value_si/2.0 for T in Ts: - xdata.append([T, rxn.get_enthalpy_of_reaction(T)]) + xdata.append([T, rxn.get_enthalpy_of_reaction(298.0)]) ydata.append(np.log(rxn.get_rate_coefficient(T))) sigmas.append(s / (8.314 * T)) From f58e7d57f1945dfc3b90abfe5c69e74e2196d18b Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 18 Jun 2022 12:38:56 -0700 Subject: [PATCH 048/109] add functions for solvent correcting kinetics --- rmgpy/data/kinetics/depository.py | 24 ++++++++++- rmgpy/data/kinetics/family.py | 66 +++++++++++++++++++++++++++++++ rmgpy/data/kinetics/library.py | 24 ++++++++++- 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/rmgpy/data/kinetics/depository.py b/rmgpy/data/kinetics/depository.py index 188f476de1..b299a81152 100644 --- a/rmgpy/data/kinetics/depository.py +++ b/rmgpy/data/kinetics/depository.py @@ -107,12 +107,34 @@ def get_source(self): """ return self.depository.label + def apply_solvent_correction(self, solvent): + """ + apply kinetic solvent correction + """ + from rmgpy.data.rmg import get_db + solvation_database = get_db('solvation') + solvent_data = solvation_database.get_solvent_data(solvent) + solute_data = self.kinetics.solute + correction = solvation_database.get_solvation_correction(solute_data, solvent_data) + dHR = 0.0 + dSR = 0.0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc) + spc_correction = solvation_database.get_solvation_correction(spc_solute_data, solvent_data) + dHR += spc_correction.enthalpy + dSR += spc_correction.entropy + + dH = correction.enthalpy-dHR + dA = np.exp((correction.entropy-dSR)/constants.R) + self.kinetics.Ea.value_si += dH + self.kinetics.A.value_si *= dA + self.kinetics.comment += "\nsolvation correction raised barrier by {0} kcal/mol and prefactor by factor of {1}".format(dH/4184.0,dA) ################################################################################ class KineticsDepository(Database): """ - A class for working with an RMG kinetics depository. Each depository + A class for working with an RMG kinetics depository. Each depository corresponds to a reaction family (a :class:`KineticsFamily` object). Each entry in a kinetics depository involves a reaction defined either by a real reactant and product species (as in a kinetics library). diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 1db06383d8..fca3e4260d 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -64,6 +64,8 @@ from rmgpy.species import Species from rmgpy.tools.uncertainty import KineticParameterUncertainty from rmgpy.molecule.fragment import Fragment +import rmgpy.constants as constants +from rmgpy.data.solvation import SoluteData, add_solute_data ################################################################################ @@ -211,6 +213,70 @@ def copy(self): return other + def apply_solvent_correction(self, solvent): + """ + apply kinetic solvent correction in this case the parameters are dGTSsite instead of GTS + """ + from rmgpy.data.rmg import get_db + solvation_database = get_db('solvation') + solvent_data = solvation_database.get_solvent_data(solvent) + site_data = self.kinetics.solute + solute_data = SoluteData( + S=site_data.S, + B=site_data.B, + E=site_data.E, + L=site_data.L, + A=site_data.A, + ) + + #compute x from gas phase + GR = 0.0 + GP = 0.0 + for reactant in self.reactants: + try: + GR += reactant.get_free_energy(298.0) + except Exception: + logging.error("Problem with reactant {!r} in reaction {!s}".format(reactant, self)) + raise + for product in self.products: + try: + GP += product.get_free_energy(298.0) + except Exception: + logging.error("Problem with product {!r} in reaction {!s}".format(reactant, self)) + raise + + GTS = self.kinetics.Ea.value_si + GR + + x = abs(GTS - GR) / (abs(GP - GTS) + abs(GR - GTS)) + + dHR = 0.0 + dSR = 0.0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc) + solute_data.S += (1.0-x) * spc_solute_data.S + solute_data.B += (1.0-x) * spc_solute_data.B + solute_data.E += (1.0-x) * spc_solute_data.E + solute_data.L += (1.0-x) * spc_solute_data.L + solute_data.A += (1.0-x) * spc_solute_data.A + spc_correction = solvation_database.get_solvation_correction(spc_solute_data, solvent_data) + dHR += spc_correction.enthalpy + dSR += spc_correction.entropy + + for spc in self.products: + spc_solute_data = solvation_database.get_solute_data(spc) + solute_data.S += x * spc_solute_data.S + solute_data.B += x * spc_solute_data.B + solute_data.E += x * spc_solute_data.E + solute_data.L += x * spc_solute_data.L + solute_data.A += x * spc_solute_data.A + + correction = solvation_database.get_solvation_correction(solute_data, solvent_data) + + dH = correction.enthalpy-dHR + dA = np.exp((correction.entropy-dSR)/constants.R) + self.kinetics.Ea.value_si += dH + self.kinetics.A.value_si *= dA + self.kinetics.comment += "\nsolvation correction raised barrier by {0} kcal/mol and prefactor by factor of {1}".format(dH/4184.0,dA) ################################################################################ diff --git a/rmgpy/data/kinetics/library.py b/rmgpy/data/kinetics/library.py index 747c37d746..4399a676b6 100644 --- a/rmgpy/data/kinetics/library.py +++ b/rmgpy/data/kinetics/library.py @@ -48,7 +48,7 @@ from rmgpy.molecule import Molecule from rmgpy.reaction import Reaction from rmgpy.species import Species - +import rmgpy.constants as constants ################################################################################ @@ -219,6 +219,28 @@ def get_sticking_coefficient(self, T): return False + def apply_solvent_correction(self, solvent): + """ + apply kinetic solvent correction + """ + from rmgpy.data.rmg import get_db + solvation_database = get_db('solvation') + solvent_data = solvation_database.get_solvent_data(solvent) + solute_data = self.kinetics.solute + correction = solvation_database.get_solvation_correction(solute_data, solvent_data) + dHR = 0.0 + dSR = 0.0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc) + spc_correction = solvation_database.get_solvation_correction(spc_solute_data, solvent_data) + dHR += spc_correction.enthalpy + dSR += spc_correction.entropy + + dH = correction.enthalpy-dHR + dA = np.exp((correction.entropy-dSR)/constants.R) + self.kinetics.Ea.value_si += dH + self.kinetics.A.value_si *= dA + self.kinetics.comment += "solvation correction raised barrier by {0} kcal/mol and prefactor by factor of {1}".format(dH/4184.0,dA) ################################################################################ From a4cb1d45b375a734b660eb3e2c1a29b6140fa5d6 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 18 Jun 2022 12:40:06 -0700 Subject: [PATCH 049/109] add functionality for site specific solvent corrections --- rmgpy/data/kinetics/family.py | 80 ++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index fca3e4260d..7ccb0e328d 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4574,7 +4574,6 @@ def _make_rule(rr): mu = np.dot(ws, dlnks) / V1 s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) - return kin else: if n == 1: kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) @@ -4597,13 +4596,33 @@ def _make_rule(rr): ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 # weighted average calculations - ws = 1.0 / varis V1 = ws.sum() V2 = (ws ** 2).sum() mu = np.dot(ws, dlnks) / V1 s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + + #site solute parameters + site_datas = [get_site_solute_data(rxn) for rxn in rxns] + site_datas = [sdata for sdata in site_datas if sdata is not None] + if len(site_datas) > 0: + site_data = SoluteData( + S=0.0, + B=0.0, + E=0.0, + L=0.0, + A=0.0, + ) + for sdata in site_datas: + add_solute_data(site_data,sdata) + site_data.S /= len(site_datas) + site_data.B /= len(site_datas) + site_data.E /= len(site_datas) + site_data.L /= len(site_datas) + site_data.A /= len(site_datas) + kin.solute = site_data + return kin else: return None @@ -4722,3 +4741,60 @@ def average_kinetics(kinetics_list): comment=f"Averaged from {len(kinetics_list)} rate expressions.", ) return averaged_kinetics + +def get_site_solute_data(rxn): + """ + apply kinetic solvent correction in this case the parameters are dGTSsite instead of GTS + """ + from rmgpy.data.rmg import get_db + solvation_database = get_db('solvation') + ts_data = rxn.kinetics.solute + if ts_data: + site_data = SoluteData( + S=ts_data.S, + B=ts_data.B, + E=ts_data.E, + L=ts_data.L, + A=ts_data.A, + ) + + #compute x from gas phase + GR = 0.0 + GP = 0.0 + + for reactant in rxn.reactants: + try: + GR += reactant.thermo.get_free_energy(298.0) + except Exception: + logging.error("Problem with reactant {!r} in reaction {!s}".format(reactant, rxn)) + raise + for product in rxn.products: + try: + GP += product.thermo.get_free_energy(298.0) + except Exception: + logging.error("Problem with product {!r} in reaction {!s}".format(reactant, rxn)) + raise + + GTS = rxn.kinetics.Ea.value_si + GR + + x = abs(GTS - GR) / (abs(GP - GTS) + abs(GR - GTS)) + + for spc in rxn.reactants: + spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) + site_data.S -= (1.0-x) * spc_solute_data.S + site_data.B -= (1.0-x) * spc_solute_data.B + site_data.E -= (1.0-x) * spc_solute_data.E + site_data.L -= (1.0-x) * spc_solute_data.L + site_data.A -= (1.0-x) * spc_solute_data.A + + for spc in rxn.products: + spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) + site_data.S -= x * spc_solute_data.S + site_data.B -= x * spc_solute_data.B + site_data.E -= x * spc_solute_data.E + site_data.L -= x * spc_solute_data.L + site_data.A -= x * spc_solute_data.A + + return site_data + else: + return None From 63ea35da4ab3e2bf20a88941e6f5a18dfe347516 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 18 Jun 2022 12:41:29 -0700 Subject: [PATCH 050/109] apply_solvent_correction in fix_barrier_height --- rmgpy/reaction.pxd | 2 +- rmgpy/reaction.py | 15 ++++++++++++++- rmgpy/rmg/model.py | 13 +++++++------ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/rmgpy/reaction.pxd b/rmgpy/reaction.pxd index 1b073a1296..15feecef3f 100644 --- a/rmgpy/reaction.pxd +++ b/rmgpy/reaction.pxd @@ -115,7 +115,7 @@ cdef class Reaction: cpdef double get_surface_rate_coefficient(self, double T, double surface_site_density, double potential=?) except -2 - cpdef fix_barrier_height(self, bint force_positive=?) + cpdef fix_barrier_height(self, bint force_positive=?, str solvent=?, bint apply_solvation_correction=?) cpdef reverse_arrhenius_rate(self, Arrhenius k_forward, str reverse_units, Tmin=?, Tmax=?) diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index b66e89538b..9de4bb7ad2 100644 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -972,7 +972,7 @@ def fix_diffusion_limited_a_factor(self, T): "diffusion factor {0.2g} evaluated at {1} K.").format( diffusion_factor, T)) - def fix_barrier_height(self, force_positive=False): + def fix_barrier_height(self, force_positive=False, solvent="", apply_solvation_correction=True): """ Turns the kinetics into Arrhenius (if they were ArrheniusEP) and ensures the activation energy is at least the endothermicity @@ -1027,6 +1027,8 @@ def fix_barrier_height(self, force_positive=False): self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) self.kinetics.Ea.value_si = Ea if isinstance(self.kinetics, (Arrhenius, StickingCoefficient, ArrheniusChargeTransfer)): # SurfaceArrhenius is a subclass of Arrhenius + if apply_solvation_correction and solvent and self.kinetics.solute: + self.apply_solvent_correction(solvent) Ea = self.kinetics.Ea.value_si if H0 >= 0 and Ea < H0: self.kinetics.Ea.value_si = H0 @@ -1054,6 +1056,12 @@ def fix_barrier_height(self, force_positive=False): " kJ/mol.".format(self.kinetics.Ea.value_si / 1000., self)) self.kinetics.Ea.value_si = 0 + def apply_solvent_correction(self, solvent): + """ + apply kinetic solvent correction + """ + return NotImplementedError("solvent correction is particular to library, depository and template reactions") + def reverse_arrhenius_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): """ Reverses the given k_forward, which must be an Arrhenius type. @@ -1074,6 +1082,7 @@ def reverse_arrhenius_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None) klist[i] = kf.get_rate_coefficient(Tlist[i]) / self.get_equilibrium_constant(Tlist[i]) kr = Arrhenius() kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + kr.solute = kf.solute return kr def reverse_surface_arrhenius_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): @@ -1097,6 +1106,7 @@ def reverse_surface_arrhenius_rate(self, k_forward, reverse_units, Tmin=None, Tm klist[i] = kf.get_rate_coefficient(Tlist[i]) / self.get_equilibrium_constant(Tlist[i]) kr = SurfaceArrhenius() kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + kr.solute = kf.solute return kr def reverse_sticking_coeff_rate(self, k_forward, reverse_units, surface_site_density, Tmin=None, Tmax=None): @@ -1123,6 +1133,7 @@ def reverse_sticking_coeff_rate(self, k_forward, reverse_units, surface_site_den self.get_equilibrium_constant(Tlist[i], surface_site_density=surface_site_density) kr = SurfaceArrhenius() kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + kr.solute = kf.solute return kr def reverse_surface_charge_transfer_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): @@ -1148,6 +1159,7 @@ def reverse_surface_charge_transfer_rate(self, k_forward, reverse_units, Tmin=No klist[i] = kf.get_rate_coefficient(Tlist[i],V0) / self.get_equilibrium_constant(Tlist[i],V0) kr = SurfaceChargeTransfer(alpha=kf.alpha.value, electrons=-1*self.electrons, V0=(V0,'V')) kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + kr.solute = kf.solute return kr def reverse_arrhenius_charge_transfer_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): @@ -1171,6 +1183,7 @@ def reverse_arrhenius_charge_transfer_rate(self, k_forward, reverse_units, Tmin= klist[i] = kf.get_rate_coefficient(Tlist[i],V0) / self.get_equilibrium_constant(Tlist[i],V0) kr = ArrheniusChargeTransfer(alpha=kf.alpha.value, electrons=-1*self.electrons, V0=(V0,'V')) kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + kr.solute = kf.solute return kr def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, Tmax=None, surface_site_density=0): diff --git a/rmgpy/rmg/model.py b/rmgpy/rmg/model.py index 7c2f5c5c88..72b95b9bad 100644 --- a/rmgpy/rmg/model.py +++ b/rmgpy/rmg/model.py @@ -546,17 +546,18 @@ def make_new_reaction(self, forward, check_existing=True, generate_thermo=True, if isinstance(forward.kinetics, KineticsData): forward.kinetics = forward.kinetics.to_arrhenius() # correct barrier heights of estimated kinetics - if isinstance(forward, (TemplateReaction, DepositoryReaction)): # i.e. not LibraryReaction - forward.fix_barrier_height() # also converts ArrheniusEP to Arrhenius. + if isinstance(forward, (TemplateReaction,DepositoryReaction)): # i.e. not LibraryReaction + forward.fix_barrier_height(solvent=self.solvent_name) # also converts ArrheniusEP to Arrhenius. elif isinstance(forward, LibraryReaction) and forward.is_surface_reaction(): # do fix the library reaction barrier if this is scaled from another metal if any(['Binding energy corrected by LSR' in x.thermo.comment for x in forward.reactants + forward.products]): - forward.fix_barrier_height() - + forward.fix_barrier_height(solvent=self.solvent_name) + elif forward.kinetics.solute: + forward.apply_solvent_correction(solvent=self.solvent_name) if self.pressure_dependence and forward.is_unimolecular(): # If this is going to be run through pressure dependence code, # we need to make sure the barrier is positive. - forward.fix_barrier_height(force_positive=True) + forward.fix_barrier_height(force_positive=True,solvent="") # Since the reaction is new, add it to the list of new reactions self.new_reaction_list.append(forward) @@ -1689,7 +1690,7 @@ def add_seed_mechanism_to_core(self, seed_mechanism, react=False): for spec in itertools.chain(rxn.reactants, rxn.products): submit(spec, self.solvent_name) - rxn.fix_barrier_height(force_positive=True) + rxn.fix_barrier_height(force_positive=True, solvent=self.solvent_name) self.add_reaction_to_core(rxn) # Check we didn't introduce unmarked duplicates From 9a00ce14ef03608c1844263025cb2421985faef1 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 18 Jun 2022 12:42:54 -0700 Subject: [PATCH 051/109] enable solvent parameters to be directly specified in input file --- rmgpy/rmg/input.py | 17 +++++++++++++---- rmgpy/rmg/main.py | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/rmgpy/rmg/input.py b/rmgpy/rmg/input.py index 8219fcb66c..de1b3e237a 100644 --- a/rmgpy/rmg/input.py +++ b/rmgpy/rmg/input.py @@ -51,6 +51,7 @@ from rmgpy.rmg.reactors import Reactor, ConstantVIdealGasReactor, ConstantTLiquidSurfaceReactor, ConstantTVLiquidReactor, ConstantTPIdealGasReactor from rmgpy.data.vaporLiquidMassTransfer import liquidVolumetricMassTransferCoefficientPowerLaw from rmgpy.molecule.fragment import Fragment +from rmgpy.data.solvation import SolventData ################################################################################ @@ -1141,11 +1142,18 @@ def simulator(atol, rtol, sens_atol=1e-6, sens_rtol=1e-4): rmg.simulator_settings_list.append(SimulatorSettings(atol, rtol, sens_atol, sens_rtol)) -def solvation(solvent): +def solvation(solvent,solventData=None): # If solvation module in input file, set the RMG solvent variable - if not isinstance(solvent, str): - raise InputError("solvent should be a string like 'water'") - rmg.solvent = solvent + #either a string corresponding to the solvent database or a olvent object + if isinstance(solvent, str): + rmg.solvent = solvent + else: + raise InputError("Solvent not specified properly, solvent must be string") + + if isinstance(solventData, SolventData) or solventData is None: + rmg.solvent_data = solventData + else: + raise InputError("Solvent not specified properly, solventData must be None or SolventData object") def model(toleranceMoveToCore=None, toleranceRadMoveToCore=np.inf, @@ -1544,6 +1552,7 @@ def read_input_file(path, rmg0): 'mbsampledReactor': mb_sampled_reactor, 'simulator': simulator, 'solvation': solvation, + 'SolventData' : SolventData, 'liquidVolumetricMassTransferCoefficientPowerLaw': liquid_volumetric_mass_transfer_coefficient_power_law, 'model': model, 'quantumMechanics': quantum_mechanics, diff --git a/rmgpy/rmg/main.py b/rmgpy/rmg/main.py index 873991baef..b7ac5b6477 100644 --- a/rmgpy/rmg/main.py +++ b/rmgpy/rmg/main.py @@ -115,6 +115,7 @@ class RMG(util.Subject): `kinetics_depositories` The kinetics depositories to use for looking up kinetics in each family `kinetics_estimator` The method to use to estimate kinetics: currently, only 'rate rules' is supported `solvent` If solvation estimates are required, the name of the solvent. + `solvent_data` The parameters assocciated with the solvent `liquid_volumetric_mass_transfer_coefficient_power_law` If kLA estimates are required, the coefficients for kLA power law ---------------------------------------------------------- ------------------------------------------------ `reaction_model` The core-edge reaction model generated by this job @@ -186,6 +187,7 @@ def clear(self): self.kinetics_depositories = None self.kinetics_estimator = "rate rules" self.solvent = None + self.solvent_data = None self.diffusion_limiter = None self.surface_site_density = None self.binding_energies = None @@ -586,9 +588,28 @@ def initialize(self, **kwargs): # Do all liquid-phase startup things: if self.solvent: - solvent_data = self.database.solvation.get_solvent_data(self.solvent) - self.reaction_model.core.phase_system.phases["Default"].set_solvent(solvent_data) - self.reaction_model.edge.phase_system.phases["Default"].set_solvent(solvent_data) + if self.solvent_data is None: + solvent_data = self.database.solvation.get_solvent_data(self.solvent) + self.solvent_data = solvent_data + solvent_mol = True + else: + solvent_data = self.solvent_data + index = 1+max([entry.index for entry in self.database.solvation.libraries['solvent'].entries.values()]) + self.database.solvation.libraries['solvent'].load_entry( + index, + self.solvent, + solvent_data, + dataCount=None, + molecule=None, + reference=None, + referenceType='', + shortDesc='', + longDesc='', + ) + solvent_mol = False + if not self.reaction_model.core.phase_system.in_nose: + self.reaction_model.core.phase_system.phases["Default"].set_solvent(solvent_data) + self.reaction_model.edge.phase_system.phases["Default"].set_solvent(solvent_data) diffusion_limiter.enable(solvent_data, self.database.solvation) logging.info("Setting solvent data for {0}".format(self.solvent)) @@ -662,9 +683,13 @@ def initialize(self, **kwargs): # For liquidReactor, checks whether the solvent is listed as one of the initial species. if self.solvent: - solvent_structure_list = self.database.solvation.get_solvent_structure(self.solvent) - for spc in solvent_structure_list: - self.database.solvation.check_solvent_in_initial_species(self, spc) + if not solvent_mol: + logging.warn("Solvent molecular structure not specified, assuming simulation is appropriate") + else: + solvent_structure_list = self.database.solvation.get_solvent_structure(self.solvent) + for spc in solvent_structure_list: + self.database.solvation.check_solvent_in_initial_species(self, spc) + # Check to see if user has input Singlet O2 into their input file or libraries # This constraint is special in that we only want to check it once in the input instead of every time a species is made From d6810aade8f234fb8036e5edacb4ed273face937 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:39:28 -0800 Subject: [PATCH 052/109] fix KineticsModel outputs --- rmgpy/kinetics/model.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rmgpy/kinetics/model.pyx b/rmgpy/kinetics/model.pyx index 9624afc8e8..262b33142b 100644 --- a/rmgpy/kinetics/model.pyx +++ b/rmgpy/kinetics/model.pyx @@ -139,14 +139,14 @@ cdef class KineticsModel: Return a string representation that can be used to reconstruct the KineticsModel object. """ - return 'KineticsModel(Tmin={0!r}, Tmax={1!r}, Pmin={2!r}, Pmax={3!r}, uncertainty={4!r}, comment="""{5}""")'.format( - self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.solute, self.uncertainty, self.comment) + return 'KineticsModel(Tmin={0!r}, Tmax={1!r}, Pmin={2!r}, Pmax={3!r}, uncertainty={4!r}, solute={5!r}, comment="""{6}""")'.format( + self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.uncertainty, self.solute, self.comment) def __reduce__(self): """ A helper function used when pickling a KineticsModel object. """ - return (KineticsModel, (self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.solute, self.uncertainty, self.comment)) + return (KineticsModel, (self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.uncertainty, self.solute, self.comment)) property Tmin: """The minimum temperature at which the model is valid, or ``None`` if not defined.""" From 0f6ab88f848841bd99c3c82ac1d1ac5a9fc84f79 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:40:42 -0800 Subject: [PATCH 053/109] add SoluteTSData object --- rmgpy/data/solvation.py | 230 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index c9746090af..3286d144c1 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -535,6 +535,236 @@ def set_mcgowan_volume(self, species): self.V = Vtot / 100 # division by 100 to get units correct. +class SoluteTSData(object): + """ + Stores Abraham parameters to characterize a solute + """ + # Set class variable with McGowan volumes + mcgowan_volumes = { + 1: 8.71, 2: 6.75, 3: 22.23, + 6: 16.35, 7: 14.39, 8: 12.43, 9: 10.47, 10: 8.51, + 14: 26.83, 15: 24.87, 16: 22.91, 17: 20.95, 18: 18.99, + 35: 26.21, 53: 34.53, + } + + def __init__(self, Sg_g=0.0, Bg_g=0.0, Eg_g=0.0, Lg_g=0.0, Ag_g=0.0, Cg_g=0.0, Sh_g=0.0, Bh_g=0.0, Eh_g=0.0, Lh_g=0.0, Ah_g=0.0, Ch_g=0.0, + K_g=0.0, Sg_h=0.0, Bg_h=0.0, Eg_h=0.0, Lg_h=0.0, Ag_h=0.0, Cg_h=0.0, Sh_h=0.0, Bh_h=0.0, Eh_h=0.0, Lh_h=0.0, Ah_h=0.0, Ch_h=0.0, K_h=0.0, comment=None): + """ + Xi_j correction is associated with calculating j (Gibbs or enthalpy) using solvent parameters for i (abraharm=g, mintz=h) + """ + self.Sg_g = Sg_g + self.Bg_g = Bg_g + self.Eg_g = Eg_g + self.Lg_g = Lg_g + self.Ag_g = Ag_g + self.Cg_g = Cg_g + self.Sh_g = Sh_g + self.Bh_g = Bh_g + self.Eh_g = Eh_g + self.Lh_g = Lh_g + self.Ah_g = Ah_g + self.Ch_g = Ch_g + self.K_g = K_g + + self.Sg_h = Sg_h + self.Bg_h = Bg_h + self.Eg_h = Eg_h + self.Lg_h = Lg_h + self.Ag_h = Ag_h + self.Cg_h = Cg_h + self.Sh_h = Sh_h + self.Bh_h = Bh_h + self.Eh_h = Eh_h + self.Lh_h = Lh_h + self.Ah_h = Ah_h + self.Ch_h = Ch_h + self.K_h = K_h + + self.comment = comment + + def __repr__(self): + return "SoluteTSData(Sg_g={0},Bg_g={1},Eg_g={2},Lg_g={3},Ag_g={4},Cg_g={5},Sh_g={6},Bh_g={7},Eh_g={8},Lh_g={9},Ah_g={10},Ch_g={11},K_g={12},Sg_h={13},Bg_h={14},Eg_h={15},Lg_h={16},Ag_h={17},Cg_h={18},Sh_h={19},Bh_h={20},Eh_h={21},Lh_h={22},Ah_h={23},Ch_h={24},K_h={25},comment={26!r})".format( + self.Sg_g, + self.Bg_g, + self.Eg_g, + self.Lg_g, + self.Ag_g, + self.Cg_g, + self.Sh_g, + self.Bh_g, + self.Eh_g, + self.Lh_g, + self.Ah_g, + self.Ch_g, + self.K_g, + self.Sg_h, + self.Bg_h, + self.Eg_h, + self.Lg_h, + self.Ag_h, + self.Cg_h, + self.Sh_h, + self.Bh_h, + self.Eh_h, + self.Lh_h, + self.Ah_h, + self.Ch_h, + self.K_h, self.comment) + + def __add__(self,sol): + return SoluteTSData( + Sg_g = self.Sg_g+sol.Sg_g, + Bg_g = self.Bg_g+sol.Bg_g, + Eg_g = self.Eg_g+sol.Eg_g, + Lg_g = self.Lg_g+sol.Lg_g, + Ag_g = self.Ag_g+sol.Ag_g, + Cg_g = self.Cg_g+sol.Cg_g, + Sh_g = self.Sh_g+sol.Sh_g, + Bh_g = self.Bh_g+sol.Bh_g, + Eh_g = self.Eh_g+sol.Eh_g, + Lh_g = self.Lh_g+sol.Lh_g, + Ah_g = self.Ah_g+sol.Ah_g, + Ch_g = self.Ch_g+sol.Ch_g, + K_g = self.K_g+sol.K_g, + Sg_h = self.Sg_h+sol.Sg_h, + Bg_h = self.Bg_h+sol.Bg_h, + Eg_h = self.Eg_h+sol.Eg_h, + Lg_h = self.Lg_h+sol.Lg_h, + Ag_h = self.Ag_h+sol.Ag_h, + Cg_h = self.Cg_h+sol.Cg_h, + Sh_h = self.Sh_h+sol.Sh_h, + Bh_h = self.Bh_h+sol.Bh_h, + Eh_h = self.Eh_h+sol.Eh_h, + Lh_h = self.Lh_h+sol.Lh_h, + Ah_h = self.Ah_h+sol.Ah_h, + Ch_h = self.Ch_h+sol.Ch_h, + K_h = self.K_h+sol.K_h, + ) + + def __sub__(self,sol): + return SoluteTSData( + Sg_g = self.Sg_g-sol.Sg_g, + Bg_g = self.Bg_g-sol.Bg_g, + Eg_g = self.Eg_g-sol.Eg_g, + Lg_g = self.Lg_g-sol.Lg_g, + Ag_g = self.Ag_g-sol.Ag_g, + Cg_g = self.Cg_g-sol.Cg_g, + Sh_g = self.Sh_g-sol.Sh_g, + Bh_g = self.Bh_g-sol.Bh_g, + Eh_g = self.Eh_g-sol.Eh_g, + Lh_g = self.Lh_g-sol.Lh_g, + Ah_g = self.Ah_g-sol.Ah_g, + Ch_g = self.Ch_g-sol.Ch_g, + K_g = self.K_g-sol.K_g, + Sg_h = self.Sg_h-sol.Sg_h, + Bg_h = self.Bg_h-sol.Bg_h, + Eg_h = self.Eg_h-sol.Eg_h, + Lg_h = self.Lg_h-sol.Lg_h, + Ag_h = self.Ag_h-sol.Ag_h, + Cg_h = self.Cg_h-sol.Cg_h, + Sh_h = self.Sh_h-sol.Sh_h, + Bh_h = self.Bh_h-sol.Bh_h, + Eh_h = self.Eh_h-sol.Eh_h, + Lh_h = self.Lh_h-sol.Lh_h, + Ah_h = self.Ah_h-sol.Ah_h, + Ch_h = self.Ch_h-sol.Ch_h, + K_h = self.K_h-sol.K_h, + ) + + def __eq__(self,sol): + if self.Sg_g != sol.Sg_g: + return False + elif self.Bg_g != sol.Bg_g: + return False + elif self.Eg_g != sol.Eg_g: + return False + elif self.Lg_g != sol.Lg_g: + return False + elif self.Ag_g != sol.Ag_g: + return False + elif self.Cg_g != sol.Cg_g: + return False + elif self.Sh_g != sol.Sh_g: + return False + elif self.Bh_g != sol.Bh_g: + return False + elif self.Eh_g != sol.Eh_g: + return False + elif self.Lh_g != sol.Lh_g: + return False + elif self.Ah_g != sol.Ah_g: + return False + elif self.Ch_g != sol.Ch_g: + return False + elif self.K_g != sol.K_g: + return False + elif self.Sg_h != sol.Sg_h: + return False + elif self.Bg_h != sol.Bg_h: + return False + elif self.Eg_h != sol.Eg_h: + return False + elif self.Lg_h != sol.Lg_h: + return False + elif self.Ag_h != sol.Ag_h: + return False + elif self.Cg_h != sol.Cg_h: + return False + elif self.Sh_h != sol.Sh_h: + return False + elif self.Bh_h != sol.Bh_h: + return False + elif self.Eh_h != sol.Eh_h: + return False + elif self.Lh_h != sol.Lh_h: + return False + elif self.Ah_h != sol.Ah_h: + return False + elif self.Ch_h != sol.Ch_h: + return False + elif self.K_h != sol.K_h: + return False + else: + return True + + def __mul__(self,num): + return SoluteTSData( + Sg_g = self.Sg_g*num, + Bg_g = self.Bg_g*num, + Eg_g = self.Eg_g*num, + Lg_g = self.Lg_g*num, + Ag_g = self.Ag_g*num, + Cg_g = self.Cg_g*num, + Sh_g = self.Sh_g*num, + Bh_g = self.Bh_g*num, + Eh_g = self.Eh_g*num, + Lh_g = self.Lh_g*num, + Ah_g = self.Ah_g*num, + Ch_g = self.Ch_g*num, + K_g = self.K_g*num, + Sg_h = self.Sg_h*num, + Bg_h = self.Bg_h*num, + Eg_h = self.Eg_h*num, + Lg_h = self.Lg_h*num, + Ag_h = self.Ag_h*num, + Cg_h = self.Cg_h*num, + Sh_h = self.Sh_h*num, + Bh_h = self.Bh_h*num, + Eh_h = self.Eh_h*num, + Lh_h = self.Lh_h*num, + Ah_h = self.Ah_h*num, + Ch_h = self.Ch_h*num, + K_h = self.K_h*num, + ) + + def calculate_corrections(self,solv): + dG298 = 0.0 + dG298 += -(np.log(10)*8.314*298.15)*(self.Sg_g*solv.s_g+self.Bg_g*solv.b_g+self.Eg_g*solv.e_g+self.Lg_g*solv.l_g+self.Ag_g*solv.a_g+self.Cg_g*solv.c_g+self.K_g) + dG298 += 1000.0*(self.Sh_g*solv.s_h+self.Bh_g*solv.b_h+self.Eh_g*solv.e_h+self.Lh_g*solv.l_h+self.Ah_g*solv.a_h+self.Ch_g*solv.c_h) + dH298 = 0.0 + dH298 += -(np.log(10)*8.314*298.15)*(self.Sg_h*solv.s_g+self.Bg_h*solv.b_g+self.Eg_h*solv.e_g+self.Lg_h*solv.l_g+self.Ag_h*solv.a_g+self.Cg_h*solv.c_g+self.K_h) + dH298 += 1000.0*(self.Sh_h*solv.s_h+self.Bh_h*solv.b_h+self.Eh_h*solv.e_h+self.Lh_h*solv.l_h+self.Ah_h*solv.a_h+self.Ch_h*solv.c_h) + return dG298,dH298 class DataCountGAV(object): """ A class for storing the number of data used to fit each solute parameter group value in the solute group additivity. From e35980c006f692c930cdf401f96da7674a4f3d33 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:41:10 -0800 Subject: [PATCH 054/109] add SoluteTSDIffData object --- rmgpy/data/solvation.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index 3286d144c1..bdb71f56e3 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -765,6 +765,41 @@ def calculate_corrections(self,solv): dH298 += -(np.log(10)*8.314*298.15)*(self.Sg_h*solv.s_g+self.Bg_h*solv.b_g+self.Eg_h*solv.e_g+self.Lg_h*solv.l_g+self.Ag_h*solv.a_g+self.Cg_h*solv.c_g+self.K_h) dH298 += 1000.0*(self.Sh_h*solv.s_h+self.Bh_h*solv.b_h+self.Eh_h*solv.e_h+self.Lh_h*solv.l_h+self.Ah_h*solv.a_h+self.Ch_h*solv.c_h) return dG298,dH298 + +class SoluteTSDiffData(object): + """ + Stores Abraham parameters to characterize a solute + """ + # Set class variable with McGowan volumes + mcgowan_volumes = { + 1: 8.71, 2: 6.75, 3: 22.23, + 6: 16.35, 7: 14.39, 8: 12.43, 9: 10.47, 10: 8.51, + 14: 26.83, 15: 24.87, 16: 22.91, 17: 20.95, 18: 18.99, + 35: 26.21, 53: 34.53, + } + + def __init__(self, S_g=None, B_g=None, E_g=None, L_g=None, A_g=None, + K_g=None, S_h=None, B_h=None, E_h=None, L_h=None, A_h=None, K_h=None, comment=None): + self.S_g = S_g + self.B_g = B_g + self.E_g = E_g + self.L_g = L_g + self.A_g = A_g + self.K_g = K_g + self.S_h = S_h + self.B_h = B_h + self.E_h = E_h + self.L_h = L_h + self.A_h = A_h + self.K_h = K_h + + self.comment=comment + + def __repr__(self): + return "SoluteTSDiffData(S_g={0},B_g={1},E_g={2},L_g={3},A_g={4},K_g={5},S_h={6},B_h={7},E_h={8},L_h={9},A_h={10},K_h={11},comment={12!r})".format( + self.S_g, self.B_g, self.E_g, self.L_g, self.A_g, self.K_g, self.S_h, self.B_h, self.E_h, + self.L_h, self.A_h, self.K_h, self.comment) + class DataCountGAV(object): """ A class for storing the number of data used to fit each solute parameter group value in the solute group additivity. From a9e29bf126af3cef20b4efddef56337735eef655 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:43:03 -0800 Subject: [PATCH 055/109] add function to convert SoluteData and SoluteTSDiffData to SoluteTSData --- rmgpy/data/solvation.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index bdb71f56e3..276f338d1b 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -800,6 +800,34 @@ def __repr__(self): self.S_g, self.B_g, self.E_g, self.L_g, self.A_g, self.K_g, self.S_h, self.B_h, self.E_h, self.L_h, self.A_h, self.K_h, self.comment) +def to_soluteTSdata(data,reactants=None): + if isinstance(data,SoluteTSData): + return data + elif isinstance(data,SoluteData): + return SoluteTSData(Sg_g=data.S,Bg_g=data.B,Eg_g=data.E,Lg_g=data.L,Ag_g=data.A,Cg_g=1.0, + Sh_h=data.S,Bh_h=data.B,Eh_h=data.E,Lh_h=data.L,Ah_h=data.A,Ch_h=1.0,comment=data.comment) + elif isinstance(data,SoluteTSDiffData): + from rmgpy.data.rmg import get_db + solvation_database = get_db('solvation') + react_data = [solvation_database.get_solute_data(spc.copy(deep=True)) for spc in reactants] + return SoluteTSData(Sg_g=data.S_g+sum([x.S for x in react_data]), + Bg_g=data.B_g+sum([x.B for x in react_data]), + Eg_g=data.E_g+sum([x.E for x in react_data]), + Lg_g=data.L_g+sum([x.L for x in react_data]), + Ag_g=data.A_g+sum([x.A for x in react_data]), + K_g=data.K_g, + Sg_h=data.S_h, + Bg_h=data.B_h, + Eg_h=data.E_h, + Lg_h=data.L_h, + Ag_h=data.A_h, + Sh_h=sum([x.S for x in react_data]), + Bh_h=sum([x.B for x in react_data]), + Eh_h=sum([x.E for x in react_data]), + Lh_h=sum([x.L for x in react_data]), + Ah_h=sum([x.A for x in react_data]), + K_h=data.K_h,comment=data.comment) + class DataCountGAV(object): """ A class for storing the number of data used to fit each solute parameter group value in the solute group additivity. From 5196b53b2d8ff71f33edab7438aa1fd44a72c287 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:46:01 -0800 Subject: [PATCH 056/109] Enable LibraryReaction to handle SoluteTSData --- rmgpy/data/kinetics/library.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rmgpy/data/kinetics/library.py b/rmgpy/data/kinetics/library.py index 4399a676b6..014e799309 100644 --- a/rmgpy/data/kinetics/library.py +++ b/rmgpy/data/kinetics/library.py @@ -226,8 +226,10 @@ def apply_solvent_correction(self, solvent): from rmgpy.data.rmg import get_db solvation_database = get_db('solvation') solvent_data = solvation_database.get_solvent_data(solvent) - solute_data = self.kinetics.solute - correction = solvation_database.get_solvation_correction(solute_data, solvent_data) + solute_data = to_soluteTSdata(self.kinetics.solute,reactants=self.reactants) + dGTS,dHTS = solute_data.calculate_corrections(solvent_data) + dSTS = (dHTS - dGTS)/298.0 + dHR = 0.0 dSR = 0.0 for spc in self.reactants: @@ -236,8 +238,8 @@ def apply_solvent_correction(self, solvent): dHR += spc_correction.enthalpy dSR += spc_correction.entropy - dH = correction.enthalpy-dHR - dA = np.exp((correction.entropy-dSR)/constants.R) + dH = dHTS-dHR + dA = np.exp((dSTS-dSR)/constants.R) self.kinetics.Ea.value_si += dH self.kinetics.A.value_si *= dA self.kinetics.comment += "solvation correction raised barrier by {0} kcal/mol and prefactor by factor of {1}".format(dH/4184.0,dA) From 2b6ce7e57e398309ea1169855295bb4f4e116489 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:48:56 -0800 Subject: [PATCH 057/109] Enable TemplateReaction to handle new TS solvent corrections --- rmgpy/data/kinetics/family.py | 46 +++++++++++++---------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 7ccb0e328d..8584ec9072 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -56,7 +56,7 @@ ForbiddenStructureException, UndeterminableKineticsError from rmgpy.kinetics import Arrhenius, SurfaceArrhenius, SurfaceArrheniusBEP, StickingCoefficient, \ StickingCoefficientBEP, ArrheniusBM, SurfaceChargeTransfer, ArrheniusChargeTransfer, \ - ArrheniusChargeTransferBM + ArrheniusChargeTransferBM, KineticsModel from rmgpy.kinetics.uncertainties import RateUncertainty, rank_accuracy_map from rmgpy.molecule import Bond, GroupBond, Group, Molecule from rmgpy.molecule.atomtype import ATOMTYPES @@ -65,7 +65,7 @@ from rmgpy.tools.uncertainty import KineticParameterUncertainty from rmgpy.molecule.fragment import Fragment import rmgpy.constants as constants -from rmgpy.data.solvation import SoluteData, add_solute_data +from rmgpy.data.solvation import SoluteData, add_solute_data, SoluteTSData, to_soluteTSdata ################################################################################ @@ -221,13 +221,6 @@ def apply_solvent_correction(self, solvent): solvation_database = get_db('solvation') solvent_data = solvation_database.get_solvent_data(solvent) site_data = self.kinetics.solute - solute_data = SoluteData( - S=site_data.S, - B=site_data.B, - E=site_data.E, - L=site_data.L, - A=site_data.A, - ) #compute x from gas phase GR = 0.0 @@ -245,35 +238,30 @@ def apply_solvent_correction(self, solvent): logging.error("Problem with product {!r} in reaction {!s}".format(reactant, self)) raise - GTS = self.kinetics.Ea.value_si + GR - - x = abs(GTS - GR) / (abs(GP - GTS) + abs(GR - GTS)) + dGrxn = GP-GR + if dGrxn > 0: + x = 1.0 + else: + x = 0.0 dHR = 0.0 dSR = 0.0 - for spc in self.reactants: - spc_solute_data = solvation_database.get_solute_data(spc) - solute_data.S += (1.0-x) * spc_solute_data.S - solute_data.B += (1.0-x) * spc_solute_data.B - solute_data.E += (1.0-x) * spc_solute_data.E - solute_data.L += (1.0-x) * spc_solute_data.L - solute_data.A += (1.0-x) * spc_solute_data.A + for spc in rxn.reactants: + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data += spc_solute_data*(1.0-x) spc_correction = solvation_database.get_solvation_correction(spc_solute_data, solvent_data) dHR += spc_correction.enthalpy dSR += spc_correction.entropy - for spc in self.products: - spc_solute_data = solvation_database.get_solute_data(spc) - solute_data.S += x * spc_solute_data.S - solute_data.B += x * spc_solute_data.B - solute_data.E += x * spc_solute_data.E - solute_data.L += x * spc_solute_data.L - solute_data.A += x * spc_solute_data.A + for spc in rxn.products: + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data += spc_solute_data*x - correction = solvation_database.get_solvation_correction(solute_data, solvent_data) + dGTS,dHTS = site_data.calculate_corrections(solvent_data) + dSTS = (dHTS - dGTS)/298.0 - dH = correction.enthalpy-dHR - dA = np.exp((correction.entropy-dSR)/constants.R) + dH = dHTS-dHR + dA = np.exp((dSTS-dSR)/constants.R) self.kinetics.Ea.value_si += dH self.kinetics.A.value_si *= dA self.kinetics.comment += "\nsolvation correction raised barrier by {0} kcal/mol and prefactor by factor of {1}".format(dH/4184.0,dA) From 3b73ed809049e9c3b04fedf702442f3e0e1c7216 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:43:53 -0800 Subject: [PATCH 058/109] Enable database to understand new objects --- rmgpy/data/kinetics/database.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rmgpy/data/kinetics/database.py b/rmgpy/data/kinetics/database.py index db3505f7c3..5c87cd8b61 100644 --- a/rmgpy/data/kinetics/database.py +++ b/rmgpy/data/kinetics/database.py @@ -46,11 +46,11 @@ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, \ Chebyshev, KineticsData, StickingCoefficient, \ StickingCoefficientBEP, SurfaceArrhenius, SurfaceArrheniusBEP, \ - ArrheniusBM, SurfaceChargeTransfer + ArrheniusBM, SurfaceChargeTransfer, KineticsModel from rmgpy.molecule import Molecule, Group from rmgpy.reaction import Reaction, same_species_lists from rmgpy.species import Species -from rmgpy.data.solvation import SoluteData +from rmgpy.data.solvation import SoluteData, SoluteTSData, SoluteTSDiffData ################################################################################ @@ -85,6 +85,9 @@ def __init__(self): 'R': constants.R, 'ArrheniusBM': ArrheniusBM, 'SoluteData': SoluteData, + 'SoluteTSData': SoluteTSData, + 'SoluteTSDiffData': SoluteTSDiffData, + 'KineticsModel': KineticsModel, } self.global_context = {} From cc7d62000b0ee8e0e9788768b840819a6e5978e1 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:51:20 -0800 Subject: [PATCH 059/109] only use kinetics that have arrhenius forms for tree generation the TS solute data is used only for rule fitting after tree is generated fix tab error --- rmgpy/data/kinetics/family.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 8584ec9072..e93daf7937 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -3272,7 +3272,7 @@ def generate_tree(self, rxns=None, obj=None, thermo_database=None, T=1000.0, npr """ if rxns is None: rxns = self.get_training_set(thermo_database=thermo_database, remove_degeneracy=True, estimate_thermo=True, - fix_labels=True, get_reverse=True) + fix_labels=True, get_reverse=True, rxns_with_kinetics_only=True) if len(rxns) <= max_batch_size: template_rxn_map = self.get_reaction_matches(rxns=rxns, thermo_database=thermo_database, remove_degeneracy=True, @@ -3850,7 +3850,7 @@ def regularize(self, regularization=simple_regularization, keep_root=True, therm if template_rxn_map is None: if rxns is None: template_rxn_map = self.get_reaction_matches(thermo_database=thermo_database, remove_degeneracy=True, - get_reverse=True, exact_matches_only=False, fix_labels=True) + get_reverse=True, exact_matches_only=False, fix_labels=True, rxns_with_kinetics_only=False) else: template_rxn_map = self.get_reaction_matches(rxns=rxns, thermo_database=thermo_database, remove_degeneracy=True, get_reverse=True, exact_matches_only=False, @@ -3949,7 +3949,7 @@ def save_generated_tree(self, path=None): self.save(path) def get_training_set(self, thermo_database=None, remove_degeneracy=False, estimate_thermo=True, fix_labels=False, - get_reverse=False): + get_reverse=False, rxns_with_kinetics_only=False): """ retrieves all reactions in the training set, assigns thermo to the species objects reverses reactions as necessary so that all reactions are in the forward direction @@ -4010,8 +4010,8 @@ def get_reactant_thermo(reactant,metal): logging.info('Must be because you turned off the training depository.') return - rxns = deepcopy([i.item for i in dep.entries.values()]) - entries = deepcopy([i for i in dep.entries.values()]) + rxns = deepcopy([i.item for i in dep.entries.values() if (not rxns_with_kinetics_only) or type(i.data) != KineticsModel]) + entries = deepcopy([i for i in dep.entries.values() if (not rxns_with_kinetics_only) or type(i.data) != KineticsModel]) roots = [x.item for x in self.get_root_template()] root = None @@ -4041,7 +4041,7 @@ def get_reactant_thermo(reactant,metal): rxns[i].kinetics = entry.data rxns[i].rank = entry.rank - if remove_degeneracy: # adjust for degeneracy + if remove_degeneracy and type(rxns[i].kinetics) != KineticsModel: # adjust for degeneracy rxns[i].kinetics.A.value_si /= rxns[i].degeneracy mol = None @@ -4111,7 +4111,10 @@ def get_reactant_thermo(reactant,metal): reacts = [Species(molecule=[get_label_fixed_mol(x.molecule[0], root_labels)], thermo=x.thermo) for x in rxns[i].reactants] - rrev = Reaction(reactants=products, products=reacts, + if type(rxns[i].kinetics) != KineticsModel: + if rxns[i].kinetics.solute: + rxns[i].kinetics.solute = to_soluteTSdata(rxns[i].kinetics.solute,reactants=rxns[i].reactants) + rrev = Reaction(reactants=products, products=reacts, kinetics=rxns[i].generate_reverse_rate_coefficient(), rank=rxns[i].rank) rrev.is_forward = False @@ -4168,7 +4171,7 @@ def get_reactant_thermo(reactant,metal): return rxns def get_reaction_matches(self, rxns=None, thermo_database=None, remove_degeneracy=False, estimate_thermo=True, - fix_labels=False, exact_matches_only=False, get_reverse=False): + fix_labels=False, exact_matches_only=False, get_reverse=False, rxns_with_kinetics_only=False): """ returns a dictionary mapping for each entry in the tree: (entry.label,entry.item) : list of all training reactions (or the list given) that match that entry @@ -4176,7 +4179,7 @@ def get_reaction_matches(self, rxns=None, thermo_database=None, remove_degenerac if rxns is None: rxns = self.get_training_set(thermo_database=thermo_database, remove_degeneracy=remove_degeneracy, estimate_thermo=estimate_thermo, fix_labels=fix_labels, - get_reverse=get_reverse) + get_reverse=get_reverse,rxns_with_kinetics_only=rxns_with_kinetics_only) entries = self.groups.entries From 1f9cc40b24739c6522756e12e46522ed5ad97ca6 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:52:05 -0800 Subject: [PATCH 060/109] enable solute data to be pulled from further up the tree as necessary --- rmgpy/data/kinetics/family.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index e93daf7937..637dfa7c8c 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -3564,6 +3564,14 @@ def make_bm_rules_from_template_rxn_map(self, template_rxn_map, nprocs=1, Tref=1 index += 1 + for label,entry in self.rules.entries.items(): #pull solute data from further up the tree as needed + entry = entry[0] + if not entry.data.solute: + ent = self.groups.entries[label] + while not self.rules.entries[ent.label][0].data.solute and ent.parent: + ent = ent.parent + entry.data.solute = self.rules.entries[ent.label][0].data.solute + def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1000.0, iters=0, random_state=1): """ Perform K-fold cross validation on an automatically generated tree at temperature T From 605d92d38e46d5d3ebc8786e8b70a5de930238a5 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:52:38 -0800 Subject: [PATCH 061/109] enable fitting of TS solute rules --- rmgpy/data/kinetics/family.py | 78 +++++++++++++---------------------- 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 637dfa7c8c..42b1cfcba7 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4542,30 +4542,31 @@ def _make_rule(rr): weights are inverse variance weights based on estimates of the error in Ln(k) for each individual reaction """ recipe, rxns, Tref, fmax, label, ranks = rr - n = len(rxns) for i, rxn in enumerate(rxns): rxn.rank = ranks[i] rxns = np.array(rxns) - data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rxns])) + rs = np.array([r for r in rxns if type(r.kinetics) != KineticsModel]) + n = len(rs) + data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rs])) if n > 0: - if isinstance(rxns[0].kinetics, Arrhenius): + if isinstance(rs[0].kinetics, Arrhenius): arr = ArrheniusBM else: arr = ArrheniusChargeTransferBM if n > 1: - kin = arr().fit_to_reactions(rxns, recipe=recipe) + kin = arr().fit_to_reactions(rs, recipe=recipe) if n == 1 or kin.E0.value_si < 0.0: - kin = average_kinetics([r.kinetics for r in rxns]) + kin = average_kinetics([r.kinetics for r in rs]) #kin.comment = "Only one reaction or Arrhenius BM fit bad. Instead averaged from {} reactions.".format(n) if n == 1: kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) else: dlnks = np.array([ np.log( - average_kinetics([r.kinetics for r in rxns[list(set(range(len(rxns))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rxns) + average_kinetics([r.kinetics for r in rs[list(set(range(len(rs))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 # weighted average calculations ws = 1.0 / varis V1 = ws.sum() @@ -4577,23 +4578,23 @@ def _make_rule(rr): if n == 1: kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) else: - if isinstance(rxns[0].kinetics, Arrhenius): + if isinstance(rs[0].kinetics, Arrhenius): dlnks = np.array([ np.log( - arr().fit_to_reactions(rxns[list(set(range(len(rxns))) - {i})], recipe=recipe) + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rxns) + ) for i, rxn in enumerate(rs) ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref else: dlnks = np.array([ np.log( - arr().fit_to_reactions(rxns[list(set(range(len(rxns))) - {i})], recipe=recipe) + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rxns) + ) for i, rxn in enumerate(rs) ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 # weighted average calculations ws = 1.0 / varis V1 = ws.sum() @@ -4606,22 +4607,11 @@ def _make_rule(rr): site_datas = [get_site_solute_data(rxn) for rxn in rxns] site_datas = [sdata for sdata in site_datas if sdata is not None] if len(site_datas) > 0: - site_data = SoluteData( - S=0.0, - B=0.0, - E=0.0, - L=0.0, - A=0.0, - ) + site_data = SoluteTSData() for sdata in site_datas: - add_solute_data(site_data,sdata) - site_data.S /= len(site_datas) - site_data.B /= len(site_datas) - site_data.E /= len(site_datas) - site_data.L /= len(site_datas) - site_data.A /= len(site_datas) + site_data += sdata + site_data = site_data * (1.0/len(site_datas)) kin.solute = site_data - return kin else: return None @@ -4749,13 +4739,7 @@ def get_site_solute_data(rxn): solvation_database = get_db('solvation') ts_data = rxn.kinetics.solute if ts_data: - site_data = SoluteData( - S=ts_data.S, - B=ts_data.B, - E=ts_data.E, - L=ts_data.L, - A=ts_data.A, - ) + site_data = to_soluteTSdata(ts_data,reactants=rxn.reactants) #compute x from gas phase GR = 0.0 @@ -4774,25 +4758,19 @@ def get_site_solute_data(rxn): logging.error("Problem with product {!r} in reaction {!s}".format(reactant, rxn)) raise - GTS = rxn.kinetics.Ea.value_si + GR - - x = abs(GTS - GR) / (abs(GP - GTS) + abs(GR - GTS)) + dGrxn = GP-GR + if dGrxn > 0: + x = 1.0 + else: + x = 0.0 for spc in rxn.reactants: - spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) - site_data.S -= (1.0-x) * spc_solute_data.S - site_data.B -= (1.0-x) * spc_solute_data.B - site_data.E -= (1.0-x) * spc_solute_data.E - site_data.L -= (1.0-x) * spc_solute_data.L - site_data.A -= (1.0-x) * spc_solute_data.A + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data -= spc_solute_data*(1.0-x) for spc in rxn.products: - spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) - site_data.S -= x * spc_solute_data.S - site_data.B -= x * spc_solute_data.B - site_data.E -= x * spc_solute_data.E - site_data.L -= x * spc_solute_data.L - site_data.A -= x * spc_solute_data.A + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data -= spc_solute_data*x return site_data else: From 1afbe5e8ec5324d05211a3abf6711b2472575503 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:55:19 -0800 Subject: [PATCH 062/109] allow training reaction notebook to handle reactions without gas phase kinetics --- ipython/kinetics_library_to_training_tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ipython/kinetics_library_to_training_tools.py b/ipython/kinetics_library_to_training_tools.py index 286b0bd32f..1d2865f242 100644 --- a/ipython/kinetics_library_to_training_tools.py +++ b/ipython/kinetics_library_to_training_tools.py @@ -185,8 +185,9 @@ def process_reactions(database, libraries, families, compare_kinetics=True, show units = 'cm^3/(mol*s)' elif len(lib_rxn.reactants) == 3: units = 'cm^6/(mol^2*s)' - A = lib_rxn.kinetics.A - lib_rxn.kinetics.A = ScalarQuantity(value=A.value_si*A.get_conversion_factor_from_si_to_cm_mol_s(),units=units,uncertainty_type=A.uncertainty_type,uncertainty=A.uncertainty_si*A.get_conversion_factor_from_si_to_cm_mol_s()) + if hasattr(lib_rxn.kinetics,'A'): + A = lib_rxn.kinetics.A + lib_rxn.kinetics.A = ScalarQuantity(value=A.value_si*A.get_conversion_factor_from_si_to_cm_mol_s(),units=units,uncertainty_type=A.uncertainty_type,uncertainty=A.uncertainty_si*A.get_conversion_factor_from_si_to_cm_mol_s()) if fam_rxn.family in reaction_dict: reaction_dict[fam_rxn.family].append(lib_rxn) @@ -436,4 +437,3 @@ def manual_selection(master_dict, multiple_dict, database): print('================================================================================') print('Manual selection of reactions completed.') print('================================================================================') - From e94af50b50f1450ffa156a28d9ab039834f49543 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 7 May 2023 12:18:29 -0700 Subject: [PATCH 063/109] added arrhenius test --- rmgpy/kinetics/arrheniusTest.py | 1197 +++++++++++++++++++++++++++++++ 1 file changed, 1197 insertions(+) create mode 100644 rmgpy/kinetics/arrheniusTest.py diff --git a/rmgpy/kinetics/arrheniusTest.py b/rmgpy/kinetics/arrheniusTest.py new file mode 100644 index 0000000000..d7d4f72974 --- /dev/null +++ b/rmgpy/kinetics/arrheniusTest.py @@ -0,0 +1,1197 @@ +#!/usr/bin/env python3 + +############################################################################### +# # +# RMG - Reaction Mechanism Generator # +# # +# Copyright (c) 2002-2021 Prof. William H. Green (whgreen@mit.edu), # +# Prof. Richard H. West (r.west@neu.edu) and the RMG Team (rmg_dev@mit.edu) # +# # +# 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 script contains unit tests of the :mod:`rmgpy.kinetics.arrhenius` module. +""" + +import math +import unittest + +import numpy as np + +import rmgpy.constants as constants +from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, ArrheniusBM, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius +from rmgpy.molecule.molecule import Molecule +from rmgpy.reaction import Reaction +from rmgpy.species import Species +from rmgpy.thermo import NASA, NASAPolynomial + + +################################################################################ + +class TestArrhenius(unittest.TestCase): + """ + Contains unit tests of the :class:`Arrhenius` class. + """ + + def setUp(self): + """ + A function run before each unit test in this class. + """ + self.A = 1.0e12 + self.n = 0.5 + self.Ea = 41.84 + self.T0 = 1. + self.Tmin = 300. + self.Tmax = 3000. + self.comment = 'C2H6' + self.arrhenius = Arrhenius( + A=(self.A, "cm^3/(mol*s)"), + n=self.n, + Ea=(self.Ea, "kJ/mol"), + T0=(self.T0, "K"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ) + + def test_a_factor(self): + """ + Test that the Arrhenius A property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.A.value_si * 1e6, self.A, delta=1e0) + + def test_n(self): + """ + Test that the Arrhenius n property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.n.value_si, self.n, 6) + + def test_ea(self): + """ + Test that the Arrhenius Ea property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.Ea.value_si * 0.001, self.Ea, 6) + + def test_temperature0(self): + """ + Test that the Arrhenius T0 property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.T0.value_si, self.T0, 6) + + def test_temperature_min(self): + """ + Test that the Arrhenius Tmin property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.Tmin.value_si, self.Tmin, 6) + + def test_temperature_max(self): + """ + Test that the Arrhenius Tmax property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.Tmax.value_si, self.Tmax, 6) + + def test_comment(self): + """ + Test that the Arrhenius comment property was properly set. + """ + self.assertEqual(self.arrhenius.comment, self.comment) + + def test_is_temperature_valid(self): + """ + Test the Arrhenius.is_temperature_valid() method. + """ + Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + validdata = np.array([False, True, True, True, True, True, True, True, True, True], np.bool) + for T, valid in zip(Tdata, validdata): + valid0 = self.arrhenius.is_temperature_valid(T) + self.assertEqual(valid0, valid) + + def test_get_rate_coefficient(self): + """ + Test the Arrhenius.get_rate_coefficient() method. + """ + Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + kexplist = np.array( + [1.6721e-4, 6.8770e1, 5.5803e3, 5.2448e4, 2.0632e5, 5.2285e5, 1.0281e6, 1.7225e6, 2.5912e6, 3.6123e6]) + for T, kexp in zip(Tlist, kexplist): + kact = self.arrhenius.get_rate_coefficient(T) + self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) + + def test_change_t0(self): + """ + Test the Arrhenius.change_t0() method. + """ + Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + k0list = np.array([self.arrhenius.get_rate_coefficient(T) for T in Tlist]) + self.arrhenius.change_t0(300) + self.assertEqual(self.arrhenius.T0.value_si, 300) + for T, kexp in zip(Tlist, k0list): + kact = self.arrhenius.get_rate_coefficient(T) + self.assertAlmostEqual(kexp, kact, delta=1e-6 * kexp) + + def test_fit_to_data(self): + """ + Test the Arrhenius.fit_to_data() method. + """ + Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + kdata = np.array([self.arrhenius.get_rate_coefficient(T) for T in Tdata]) + arrhenius = Arrhenius().fit_to_data(Tdata, kdata, kunits="m^3/(mol*s)") + self.assertEqual(float(self.arrhenius.T0.value_si), 1) + for T, k in zip(Tdata, kdata): + self.assertAlmostEqual(k, arrhenius.get_rate_coefficient(T), delta=1e-6 * k) + self.assertAlmostEqual(arrhenius.A.value_si, self.arrhenius.A.value_si, delta=1e0) + self.assertAlmostEqual(arrhenius.n.value_si, self.arrhenius.n.value_si, 1, 4) + self.assertAlmostEqual(arrhenius.Ea.value_si, self.arrhenius.Ea.value_si, 2) + self.assertAlmostEqual(arrhenius.T0.value_si, self.arrhenius.T0.value_si, 4) + + def test_fit_to_negative_data(self): + """ + Test the Arrhenius.fit_to_data() method on negative rates + """ + Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + kdata = np.array([-1 * self.arrhenius.get_rate_coefficient(T) for T in Tdata]) + arrhenius = Arrhenius().fit_to_data(Tdata, kdata, kunits="m^3/(mol*s)") + self.assertEqual(float(self.arrhenius.T0.value_si), 1) + for T, k in zip(Tdata, kdata): + self.assertAlmostEqual(k, arrhenius.get_rate_coefficient(T), delta=1e-6 * abs(k)) + self.assertAlmostEqual(arrhenius.A.value_si, -1 * self.arrhenius.A.value_si, delta=1e0) + self.assertAlmostEqual(arrhenius.n.value_si, self.arrhenius.n.value_si, 1, 4) + self.assertAlmostEqual(arrhenius.Ea.value_si, self.arrhenius.Ea.value_si, 2) + self.assertAlmostEqual(arrhenius.T0.value_si, self.arrhenius.T0.value_si, 4) + + def test_pickle(self): + """ + Test that an Arrhenius object can be pickled and unpickled with no loss + of information. + """ + import pickle + arrhenius = pickle.loads(pickle.dumps(self.arrhenius, -1)) + self.assertAlmostEqual(self.arrhenius.A.value, arrhenius.A.value, delta=1e0) + self.assertEqual(self.arrhenius.A.units, arrhenius.A.units) + self.assertAlmostEqual(self.arrhenius.n.value, arrhenius.n.value, 4) + self.assertAlmostEqual(self.arrhenius.Ea.value, arrhenius.Ea.value, 4) + self.assertEqual(self.arrhenius.Ea.units, arrhenius.Ea.units) + self.assertAlmostEqual(self.arrhenius.T0.value, arrhenius.T0.value, 4) + self.assertEqual(self.arrhenius.T0.units, arrhenius.T0.units) + self.assertAlmostEqual(self.arrhenius.Tmin.value, arrhenius.Tmin.value, 4) + self.assertEqual(self.arrhenius.Tmin.units, arrhenius.Tmin.units) + self.assertAlmostEqual(self.arrhenius.Tmax.value, arrhenius.Tmax.value, 4) + self.assertEqual(self.arrhenius.Tmax.units, arrhenius.Tmax.units) + self.assertEqual(self.arrhenius.comment, arrhenius.comment) + + def test_repr(self): + """ + Test that an Arrhenius object can be reconstructed from its repr() + output with no loss of information. + """ + namespace = {} + exec('arrhenius = {0!r}'.format(self.arrhenius), globals(), namespace) + self.assertIn('arrhenius', namespace) + arrhenius = namespace['arrhenius'] + self.assertAlmostEqual(self.arrhenius.A.value, arrhenius.A.value, delta=1e0) + self.assertEqual(self.arrhenius.A.units, arrhenius.A.units) + self.assertAlmostEqual(self.arrhenius.n.value, arrhenius.n.value, 4) + self.assertAlmostEqual(self.arrhenius.Ea.value, arrhenius.Ea.value, 4) + self.assertEqual(self.arrhenius.Ea.units, arrhenius.Ea.units) + self.assertAlmostEqual(self.arrhenius.T0.value, arrhenius.T0.value, 4) + self.assertEqual(self.arrhenius.T0.units, arrhenius.T0.units) + self.assertAlmostEqual(self.arrhenius.Tmin.value, arrhenius.Tmin.value, 4) + self.assertEqual(self.arrhenius.Tmin.units, arrhenius.Tmin.units) + self.assertAlmostEqual(self.arrhenius.Tmax.value, arrhenius.Tmax.value, 4) + self.assertEqual(self.arrhenius.Tmax.units, arrhenius.Tmax.units) + self.assertEqual(self.arrhenius.comment, arrhenius.comment) + + def test_change_rate(self): + """ + Test the Arrhenius.change_rate() method. + """ + Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + k0list = np.array([self.arrhenius.get_rate_coefficient(T) for T in Tlist]) + self.arrhenius.change_rate(2) + for T, kexp in zip(Tlist, k0list): + kact = self.arrhenius.get_rate_coefficient(T) + self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) + + def test_to_cantera_kinetics(self): + """ + Test that the Arrhenius cantera object can be set properly within + a cantera Reaction object + """ + ctArrhenius = self.arrhenius.to_cantera_kinetics() + self.assertAlmostEqual(ctArrhenius.pre_exponential_factor, 1e9, 6) + self.assertAlmostEqual(ctArrhenius.temperature_exponent, 0.5) + self.assertAlmostEqual(ctArrhenius.activation_energy, 41.84e6) + + def test_to_arrhenius_ep(self): + """ + Tests that the Arrhenius object can be converted to ArrheniusEP + """ + arr_rate = self.arrhenius.get_rate_coefficient(500) + arr_ep = self.arrhenius.to_arrhenius_ep() + arr_ep_rate = arr_ep.get_rate_coefficient(500, 10) # the second number should not matter + self.assertAlmostEqual(arr_rate, arr_ep_rate) + + def test_to_arrhenius_ep_with_alpha_and_hrxn(self): + """ + Tests that the Arrhenius object can be converted to ArrheniusEP given parameters + """ + hrxn = 5 + arr_rate = self.arrhenius.get_rate_coefficient(500) + arr_ep = self.arrhenius.to_arrhenius_ep(alpha=1, dHrxn=hrxn) + self.assertAlmostEqual(1., arr_ep.alpha.value_si) + arr_ep_rate = arr_ep.get_rate_coefficient(500, hrxn) + self.assertAlmostEqual(arr_rate, arr_ep_rate) + + def test_to_arrhenius_ep_throws_error_with_just_alpha(self): + with self.assertRaises(Exception): + self.arrhenius.to_arrhenius_ep(alpha=1) + + +################################################################################ + +class TestArrheniusEP(unittest.TestCase): + """ + Contains unit tests of the :class:`ArrheniusEP` class. + """ + + def setUp(self): + """ + A function run before each unit test in this class. + """ + self.A = 1.0e12 + self.n = 0.5 + self.alpha = 0.5 + self.E0 = 41.84 + self.Tmin = 300. + self.Tmax = 3000. + self.comment = 'C2H6' + self.arrhenius = ArrheniusEP( + A=(self.A, "cm^3/(mol*s)"), + n=self.n, + alpha=self.alpha, + E0=(self.E0, "kJ/mol"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ) + + def test_a_factor(self): + """ + Test that the ArrheniusEP A property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.A.value_si * 1e6, self.A, delta=1e0) + + def test_n(self): + """ + Test that the ArrheniusEP n property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.n.value_si, self.n, 6) + + def test_alpha(self): + """ + Test that the ArrheniusEP alpha property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.alpha.value_si, self.alpha, 6) + + def test_e0(self): + """ + Test that the ArrheniusEP E0 property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.E0.value_si * 0.001, self.E0, 6) + + def test_temperature_min(self): + """ + Test that the ArrheniusEP Tmin property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.Tmin.value_si, self.Tmin, 6) + + def test_temperature_max(self): + """ + Test that the ArrheniusEP Tmax property was properly set. + """ + self.assertAlmostEqual(self.arrhenius.Tmax.value_si, self.Tmax, 6) + + def test_comment(self): + """ + Test that the ArrheniusEP comment property was properly set. + """ + self.assertEqual(self.arrhenius.comment, self.comment) + + def test_is_temperature_valid(self): + """ + Test the ArrheniusEP.is_temperature_valid() method. + """ + Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + validdata = np.array([False, True, True, True, True, True, True, True, True, True], np.bool) + for T, valid in zip(Tdata, validdata): + valid0 = self.arrhenius.is_temperature_valid(T) + self.assertEqual(valid0, valid) + + def test_get_rate_coefficient(self): + """ + Test the ArrheniusEP.get_rate_coefficient() method. + """ + Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + kexplist = np.array( + [1.6721e-4, 6.8770e1, 5.5803e3, 5.2448e4, 2.0632e5, 5.2285e5, 1.0281e6, 1.7225e6, 2.5912e6, 3.6123e6]) + for T, kexp in zip(Tlist, kexplist): + kact = self.arrhenius.get_rate_coefficient(T, ) + self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) + + def test_pickle(self): + """ + Test that an ArrheniusEP object can be pickled and unpickled with no loss + of information. + """ + import pickle + arrhenius = pickle.loads(pickle.dumps(self.arrhenius, -1)) + self.assertAlmostEqual(self.arrhenius.A.value, arrhenius.A.value, delta=1e0) + self.assertEqual(self.arrhenius.A.units, arrhenius.A.units) + self.assertAlmostEqual(self.arrhenius.n.value, arrhenius.n.value, 4) + self.assertAlmostEqual(self.arrhenius.alpha.value, arrhenius.alpha.value, 4) + self.assertAlmostEqual(self.arrhenius.E0.value, arrhenius.E0.value, 4) + self.assertEqual(self.arrhenius.E0.units, arrhenius.E0.units) + self.assertAlmostEqual(self.arrhenius.Tmin.value, arrhenius.Tmin.value, 4) + self.assertEqual(self.arrhenius.Tmin.units, arrhenius.Tmin.units) + self.assertAlmostEqual(self.arrhenius.Tmax.value, arrhenius.Tmax.value, 4) + self.assertEqual(self.arrhenius.Tmax.units, arrhenius.Tmax.units) + self.assertEqual(self.arrhenius.comment, arrhenius.comment) + + def test_repr(self): + """ + Test that an ArrheniusEP object can be reconstructed from its repr() + output with no loss of information. + """ + namespace = {} + exec('arrhenius = {0!r}'.format(self.arrhenius), globals(), namespace) + self.assertIn('arrhenius', namespace) + arrhenius = namespace['arrhenius'] + self.assertAlmostEqual(self.arrhenius.A.value, arrhenius.A.value, delta=1e0) + self.assertEqual(self.arrhenius.A.units, arrhenius.A.units) + self.assertAlmostEqual(self.arrhenius.n.value, arrhenius.n.value, 4) + self.assertAlmostEqual(self.arrhenius.alpha.value, arrhenius.alpha.value, 4) + self.assertAlmostEqual(self.arrhenius.E0.value, arrhenius.E0.value, 4) + self.assertEqual(self.arrhenius.E0.units, arrhenius.E0.units) + self.assertAlmostEqual(self.arrhenius.Tmin.value, arrhenius.Tmin.value, 4) + self.assertEqual(self.arrhenius.Tmin.units, arrhenius.Tmin.units) + self.assertAlmostEqual(self.arrhenius.Tmax.value, arrhenius.Tmax.value, 4) + self.assertEqual(self.arrhenius.Tmax.units, arrhenius.Tmax.units) + self.assertEqual(self.arrhenius.comment, arrhenius.comment) + + def test_change_rate(self): + """ + Test the ArrheniusEP.change_rate() method. + """ + Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + k0list = np.array([self.arrhenius.get_rate_coefficient(T) for T in Tlist]) + self.arrhenius.change_rate(2) + for T, kexp in zip(Tlist, k0list): + kact = self.arrhenius.get_rate_coefficient(T) + self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) + + +################################################################################ + +class TestArrheniusBM(unittest.TestCase): + """ + Contains unit tests of the :class:`ArrheniusBM` class. + """ + + def setUp(self): + """ + A function run before each unit test in this class. + """ + self.A = 8.00037e+12 + self.n = 0.391734 + self.w0 = 798000 + self.E0 = 116249.32617478925 + self.Tmin = 300. + self.Tmax = 2000. + self.comment = 'rxn001084' + self.arrhenius_bm = ArrheniusBM( + A=(self.A, "s^-1"), + n=self.n, + w0=(self.w0, 'J/mol'), + E0=(self.E0, "J/mol"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ) + + self.rsmi = 'NC(=NC=O)O' + self.psmi = 'O=CNC(=O)N' + self.arrhenius = Arrhenius(A=(8.00037e+12,'s^-1'), + n=0.391734, + Ea=(94.5149,'kJ/mol'), + T0=(1,'K'), + Tmin=(300,'K'), + Tmax=(2000,'K'), + comment="""Fitted to 50 data points; dA = *|/ 1.18377, dn = +|- 0.0223855, dEa = +|- 0.115431 kJ/mol""" + ) + + self.r_thermo = NASA(polynomials=[ + NASAPolynomial(coeffs=[3.90453,0.0068491,0.000125755,-2.92973e-07,2.12971e-10,-45444.2,10.0669], Tmin=(10,'K'), Tmax=(433.425,'K')), + NASAPolynomial(coeffs=[2.09778,0.0367646,-2.36023e-05,7.24527e-09,-8.51275e-13,-45412,15.8381], Tmin=(433.425,'K'), Tmax=(3000,'K'))], + Tmin=(10,'K'), Tmax=(3000,'K'), E0=(-377.851,'kJ/mol'), Cp0=(33.2579,'J/(mol*K)'), CpInf=(232.805,'J/(mol*K)'), + comment="""Thermo library: Spiekermann_refining_elementary_reactions""" + ) + self.p_thermo = NASA(polynomials=[ + NASAPolynomial(coeffs=[3.88423,0.00825528,0.000133399,-3.31802e-07,2.52823e-10,-51045.1,10.3937], Tmin=(10,'K'), Tmax=(428.701,'K')), + NASAPolynomial(coeffs=[2.89294,0.0351772,-2.26349e-05,7.00331e-09,-8.2982e-13,-51122.5,12.4424], Tmin=(428.701,'K'), Tmax=(3000,'K'))], + Tmin=(10,'K'), Tmax=(3000,'K'), E0=(-424.419,'kJ/mol'), Cp0=(33.2579,'J/(mol*K)'), CpInf=(232.805,'J/(mol*K)'), + comment="""Thermo library: Spiekermann_refining_elementary_reactions""" + ) + + def test_a_factor(self): + """ + Test that the ArrheniusBM A property was properly set. + """ + self.assertAlmostEqual(self.arrhenius_bm.A.value_si, self.A, delta=1e0) + + def test_n(self): + """ + Test that the ArrheniusBM n property was properly set. + """ + self.assertAlmostEqual(self.arrhenius_bm.n.value_si, self.n, 6) + + def test_w0(self): + """ + Test that the ArrheniusBM w0 property was properly set. + """ + self.assertAlmostEqual(self.arrhenius_bm.w0.value_si, self.w0, 6) + + def test_e0(self): + """ + Test that the ArrheniusBM E0 property was properly set. + """ + self.assertAlmostEqual(self.arrhenius_bm.E0.value_si, self.E0, 6) + + def test_temperature_min(self): + """ + Test that the ArrheniusBM Tmin property was properly set. + """ + self.assertAlmostEqual(self.arrhenius_bm.Tmin.value_si, self.Tmin, 6) + + def test_temperature_max(self): + """ + Test that the ArrheniusBM Tmax property was properly set. + """ + self.assertAlmostEqual(self.arrhenius_bm.Tmax.value_si, self.Tmax, 6) + + def test_is_temperature_valid(self): + """ + Test the ArrheniusBM.is_temperature_valid() method. + """ + Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + validdata = np.array([False, True, True, True, True, True, True, True, True, True], np.bool) + for T, valid in zip(Tdata, validdata): + valid0 = self.arrhenius_bm.is_temperature_valid(T) + self.assertEqual(valid0, valid) + + def test_fit_to_data(self): + """ + Test the ArrheniusBM.fit_to_data() method. + """ + reactant = Molecule(smiles=self.rsmi) + product = Molecule(smiles=self.psmi) + reaction = Reaction(reactants=[Species(molecule=[reactant], thermo=self.r_thermo,)], + products=[Species(molecule=[product], thermo=self.p_thermo)], + kinetics=self.arrhenius, + ) + + arrhenius_bm = ArrheniusBM().fit_to_reactions([reaction], w0=self.w0) + self.assertAlmostEqual(arrhenius_bm.A.value_si, self.arrhenius_bm.A.value_si, delta=1.5e1) + self.assertAlmostEqual(arrhenius_bm.n.value_si, self.arrhenius_bm.n.value_si, 1, 4) + self.assertAlmostEqual(arrhenius_bm.E0.value_si, self.arrhenius_bm.E0.value_si, 1) + + def test_get_activation_energy(self): + """ + Test the ArrheniusBM.get_activation_energy() method. + """ + Hrxn = -44000 # J/mol + Ea = self.arrhenius_bm.get_activation_energy(Hrxn) + self.assertAlmostEqual(Ea, 95074, delta=1e1) + + +################################################################################ + +class TestPDepArrhenius(unittest.TestCase): + """ + Contains unit tests of the :class:`PDepArrhenius` class. + """ + + def setUp(self): + """ + A function run before each unit test in this class. + """ + self.arrhenius0 = Arrhenius( + A=(1.0e6, "s^-1"), + n=1.0, + Ea=(10.0, "kJ/mol"), + T0=(300.0, "K"), + Tmin=(300.0, "K"), + Tmax=(2000.0, "K"), + comment="""This data is completely made up""", + ) + self.arrhenius1 = Arrhenius( + A=(1.0e12, "s^-1"), + n=1.0, + Ea=(20.0, "kJ/mol"), + T0=(300.0, "K"), + Tmin=(300.0, "K"), + Tmax=(2000.0, "K"), + comment="""This data is completely made up""", + ) + self.pressures = np.array([0.1, 10.0]) + self.arrhenius = [self.arrhenius0, self.arrhenius1] + self.Tmin = 300.0 + self.Tmax = 2000.0 + self.Pmin = 0.1 + self.Pmax = 10.0 + self.comment = """This data is completely made up""" + self.kinetics = PDepArrhenius( + pressures=(self.pressures, "bar"), + arrhenius=self.arrhenius, + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + Pmin=(self.Pmin, "bar"), + Pmax=(self.Pmax, "bar"), + comment=self.comment, + ) + + def test_pressures(self): + """ + Test that the PDepArrhenius pressures property was properly set. + """ + self.assertEqual(len(self.kinetics.pressures.value_si), 2) + for i in range(2): + self.assertAlmostEqual(self.kinetics.pressures.value_si[i] * 1e-5, self.pressures[i], 4) + + def test_arrhenius(self): + """ + Test that the PDepArrhenius arrhenius property was properly set. + """ + self.assertEqual(len(self.kinetics.arrhenius), 2) + for i in range(2): + self.assertAlmostEqual(self.kinetics.arrhenius[i].A.value, self.arrhenius[i].A.value, delta=1e0) + self.assertEqual(self.kinetics.arrhenius[i].A.units, self.arrhenius[i].A.units) + self.assertAlmostEqual(self.kinetics.arrhenius[i].n.value, self.arrhenius[i].n.value, 4) + self.assertAlmostEqual(self.kinetics.arrhenius[i].Ea.value, self.arrhenius[i].Ea.value, 4) + self.assertEqual(self.kinetics.arrhenius[i].Ea.units, self.arrhenius[i].Ea.units) + self.assertAlmostEqual(self.kinetics.arrhenius[i].T0.value, self.arrhenius[i].T0.value, 4) + self.assertEqual(self.kinetics.arrhenius[i].T0.units, self.arrhenius[i].T0.units) + self.assertAlmostEqual(self.kinetics.arrhenius[i].Tmin.value, self.arrhenius[i].Tmin.value, 4) + self.assertEqual(self.kinetics.arrhenius[i].Tmin.units, self.arrhenius[i].Tmin.units) + self.assertAlmostEqual(self.kinetics.arrhenius[i].Tmax.value, self.arrhenius[i].Tmax.value, 4) + self.assertEqual(self.kinetics.arrhenius[i].Tmax.units, self.arrhenius[i].Tmax.units) + self.assertEqual(self.kinetics.arrhenius[i].comment, self.arrhenius[i].comment) + + def test_temperature_min(self): + """ + Test that the PDepArrhenius Tmin property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Tmin.value_si, self.Tmin, 6) + + def test_temperature_max(self): + """ + Test that the PDepArrhenius Tmax property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Tmax.value_si, self.Tmax, 6) + + def test_pressure_min(self): + """ + Test that the PDepArrhenius Pmin property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Pmin.value_si * 1e-5, self.Pmin, 6) + + def test_pressure_max(self): + """ + Test that the PDepArrhenius Pmax property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Pmax.value_si * 1e-5, self.Pmax, 6) + + def test_comment(self): + """ + Test that the PDepArrhenius comment property was properly set. + """ + self.assertEqual(self.kinetics.comment, self.comment) + + def test_is_pressure_dependent(self): + """ + Test the PDepArrhenius.is_pressure_dependent() method. + """ + self.assertTrue(self.kinetics.is_pressure_dependent()) + + def test_get_rate_coefficient(self): + """ + Test the PDepArrhenius.get_rate_coefficient() method. + """ + P = 1e4 + for T in [300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]: + k0 = self.kinetics.get_rate_coefficient(T, P) + k1 = self.arrhenius0.get_rate_coefficient(T) + self.assertAlmostEqual(k0, k1, delta=1e-6 * k1) + P = 1e6 + for T in [300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]: + k0 = self.kinetics.get_rate_coefficient(T, P) + k1 = self.arrhenius1.get_rate_coefficient(T) + self.assertAlmostEqual(k0, k1, delta=1e-6 * k1) + P = 1e5 + for T in [300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]: + k0 = self.kinetics.get_rate_coefficient(T, P) + k1 = math.sqrt(self.arrhenius0.get_rate_coefficient(T) * self.arrhenius1.get_rate_coefficient(T)) + self.assertAlmostEqual(k0, k1, delta=1e-6 * k1) + + def test_fit_to_data(self): + """ + Test the PDepArrhenius.fit_to_data() method. + """ + Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500], np.float) + Pdata = np.array([1e4, 3e4, 1e5, 3e5, 1e6], np.float) + kdata = np.zeros([len(Tdata), len(Pdata)], np.float) + for t in range(len(Tdata)): + for p in range(len(Pdata)): + kdata[t, p] = self.kinetics.get_rate_coefficient(Tdata[t], Pdata[p]) + kinetics = PDepArrhenius().fit_to_data(Tdata, Pdata, kdata, kunits="s^-1") + for t in range(len(Tdata)): + for p in range(len(Pdata)): + self.assertAlmostEqual(kinetics.get_rate_coefficient(Tdata[t], Pdata[p]), kdata[t, p], + delta=1e-6 * kdata[t, p]) + + def test_pickle(self): + """ + Test that a PDepArrhenius object can be successfully pickled and + unpickled with no loss of information. + """ + import pickle + kinetics = pickle.loads(pickle.dumps(self.kinetics, -1)) + Narrh = 2 + self.assertEqual(len(self.kinetics.pressures.value), Narrh) + self.assertEqual(len(kinetics.pressures.value), Narrh) + self.assertEqual(len(self.kinetics.arrhenius), Narrh) + self.assertEqual(len(kinetics.arrhenius), Narrh) + for i in range(Narrh): + self.assertAlmostEqual(self.kinetics.pressures.value[i], kinetics.pressures.value[i], 4) + self.assertAlmostEqual(self.kinetics.arrhenius[i].A.value, kinetics.arrhenius[i].A.value, delta=1e0) + self.assertEqual(self.kinetics.arrhenius[i].A.units, kinetics.arrhenius[i].A.units) + self.assertAlmostEqual(self.kinetics.arrhenius[i].n.value, kinetics.arrhenius[i].n.value) + self.assertAlmostEqual(self.kinetics.arrhenius[i].T0.value, kinetics.arrhenius[i].T0.value, 4) + self.assertEqual(self.kinetics.arrhenius[i].T0.units, kinetics.arrhenius[i].T0.units) + self.assertAlmostEqual(self.kinetics.arrhenius[i].Ea.value, kinetics.arrhenius[i].Ea.value, 4) + self.assertEqual(self.kinetics.arrhenius[i].Ea.units, kinetics.arrhenius[i].Ea.units) + self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) + self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) + self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) + self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) + self.assertAlmostEqual(self.kinetics.Pmin.value, kinetics.Pmin.value, 4) + self.assertEqual(self.kinetics.Pmin.units, kinetics.Pmin.units) + self.assertAlmostEqual(self.kinetics.Pmax.value, kinetics.Pmax.value, 4) + self.assertEqual(self.kinetics.Pmax.units, kinetics.Pmax.units) + self.assertEqual(self.kinetics.comment, kinetics.comment) + + def test_repr(self): + """ + Test that a PDepArrhenius object can be successfully reconstructed + from its repr() output with no loss of information. + """ + namespace = {} + exec('kinetics = {0!r}'.format(self.kinetics), globals(), namespace) + self.assertIn('kinetics', namespace) + kinetics = namespace['kinetics'] + Narrh = 2 + self.assertEqual(len(self.kinetics.pressures.value), Narrh) + self.assertEqual(len(kinetics.pressures.value), Narrh) + self.assertEqual(len(self.kinetics.arrhenius), Narrh) + self.assertEqual(len(kinetics.arrhenius), Narrh) + for i in range(Narrh): + self.assertAlmostEqual(self.kinetics.pressures.value[i], kinetics.pressures.value[i], 4) + self.assertAlmostEqual(self.kinetics.arrhenius[i].A.value, kinetics.arrhenius[i].A.value, delta=1e0) + self.assertEqual(self.kinetics.arrhenius[i].A.units, kinetics.arrhenius[i].A.units) + self.assertAlmostEqual(self.kinetics.arrhenius[i].n.value, kinetics.arrhenius[i].n.value) + self.assertAlmostEqual(self.kinetics.arrhenius[i].T0.value, kinetics.arrhenius[i].T0.value, 4) + self.assertEqual(self.kinetics.arrhenius[i].T0.units, kinetics.arrhenius[i].T0.units) + self.assertAlmostEqual(self.kinetics.arrhenius[i].Ea.value, kinetics.arrhenius[i].Ea.value, 4) + self.assertEqual(self.kinetics.arrhenius[i].Ea.units, kinetics.arrhenius[i].Ea.units) + self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) + self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) + self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) + self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) + self.assertAlmostEqual(self.kinetics.Pmin.value, kinetics.Pmin.value, 4) + self.assertEqual(self.kinetics.Pmin.units, kinetics.Pmin.units) + self.assertAlmostEqual(self.kinetics.Pmax.value, kinetics.Pmax.value, 4) + self.assertEqual(self.kinetics.Pmax.units, kinetics.Pmax.units) + self.assertEqual(self.kinetics.comment, kinetics.comment) + + def test_change_rate(self): + """ + Test the PDepArrhenius.change_rate() method. + """ + Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + k0list = np.array([self.kinetics.get_rate_coefficient(T, 1e5) for T in Tlist]) + self.kinetics.change_rate(2) + for T, kexp in zip(Tlist, k0list): + kact = self.kinetics.get_rate_coefficient(T, 1e5) + self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) + + +################################################################################ + +class TestMultiArrhenius(unittest.TestCase): + """ + Contains unit tests of the :class:`MultiArrhenius` class. + """ + + def setUp(self): + """ + A function run before each unit test in this class. + """ + self.Tmin = 350. + self.Tmax = 1500. + self.comment = 'Comment' + self.arrhenius = [ + Arrhenius( + A=(9.3e-14, "cm^3/(molecule*s)"), + n=0.0, + Ea=(4740 * constants.R * 0.001, "kJ/mol"), + T0=(1, "K"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ), + Arrhenius( + A=(1.4e-9, "cm^3/(molecule*s)"), + n=0.0, + Ea=(11200 * constants.R * 0.001, "kJ/mol"), + T0=(1, "K"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ), + ] + self.kinetics = MultiArrhenius( + arrhenius=self.arrhenius, + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ) + self.single_kinetics = MultiArrhenius( + arrhenius=self.arrhenius[:1], + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ) + + def test_arrhenius(self): + """ + Test that the MultiArrhenius A property was properly set. + """ + self.assertEqual(self.kinetics.arrhenius, self.arrhenius) + + def test_temperature_min(self): + """ + Test that the MultiArrhenius Tmin property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Tmin.value_si, self.Tmin, 6) + + def test_temperature_max(self): + """ + Test that the MultiArrhenius Tmax property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Tmax.value_si, self.Tmax, 6) + + def test_comment(self): + """ + Test that the MultiArrhenius comment property was properly set. + """ + self.assertEqual(self.kinetics.comment, self.comment) + + def test_is_temperature_valid(self): + """ + Test the MultiArrhenius.is_temperature_valid() method. + """ + Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + validdata = np.array([False, True, True, True, True, True, True, False, False, False], np.bool) + for T, valid in zip(Tdata, validdata): + valid0 = self.kinetics.is_temperature_valid(T) + self.assertEqual(valid0, valid) + + def test_get_rate_coefficient(self): + """ + Test the MultiArrhenius.get_rate_coefficient() method. + """ + Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + kexplist = np.array( + [2.85400e-06, 4.00384e-01, 2.73563e+01, 8.50699e+02, 1.20181e+04, 7.56312e+04, 2.84724e+05, 7.71702e+05, + 1.67743e+06, 3.12290e+06]) + for T, kexp in zip(Tlist, kexplist): + kact = self.kinetics.get_rate_coefficient(T) + self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) + + def test_pickle(self): + """ + Test that a MultiArrhenius object can be pickled and unpickled with no loss + of information. + """ + import pickle + kinetics = pickle.loads(pickle.dumps(self.kinetics, -1)) + self.assertEqual(len(self.kinetics.arrhenius), len(kinetics.arrhenius)) + for arrh0, arrh in zip(self.kinetics.arrhenius, kinetics.arrhenius): + self.assertAlmostEqual(arrh0.A.value, arrh.A.value, delta=1e-18) + self.assertEqual(arrh0.A.units, arrh.A.units) + self.assertAlmostEqual(arrh0.n.value, arrh.n.value, 4) + self.assertAlmostEqual(arrh0.Ea.value, arrh.Ea.value, 4) + self.assertEqual(arrh0.Ea.units, arrh.Ea.units) + self.assertAlmostEqual(arrh0.T0.value, arrh.T0.value, 4) + self.assertEqual(arrh0.T0.units, arrh.T0.units) + self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) + self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) + self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) + self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) + self.assertEqual(self.kinetics.comment, kinetics.comment) + + def test_repr(self): + """ + Test that a MultiArrhenius object can be reconstructed from its repr() + output with no loss of information. + """ + namespace = {} + exec('kinetics = {0!r}'.format(self.kinetics), globals(), namespace) + self.assertIn('kinetics', namespace) + kinetics = namespace['kinetics'] + self.assertEqual(len(self.kinetics.arrhenius), len(kinetics.arrhenius)) + for arrh0, arrh in zip(self.kinetics.arrhenius, kinetics.arrhenius): + self.assertAlmostEqual(arrh0.A.value, arrh.A.value, delta=1e-18) + self.assertEqual(arrh0.A.units, arrh.A.units) + self.assertAlmostEqual(arrh0.n.value, arrh.n.value, 4) + self.assertAlmostEqual(arrh0.Ea.value, arrh.Ea.value, 4) + self.assertEqual(arrh0.Ea.units, arrh.Ea.units) + self.assertAlmostEqual(arrh0.T0.value, arrh.T0.value, 4) + self.assertEqual(arrh0.T0.units, arrh.T0.units) + self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) + self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) + self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) + self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) + self.assertEqual(self.kinetics.comment, kinetics.comment) + + def test_to_arrhenius(self): + """ + Test that we can convert to an Arrhenius + """ + answer = self.single_kinetics.arrhenius[0] + fitted = self.single_kinetics.to_arrhenius() + + self.assertAlmostEqual(fitted.A.value_si, answer.A.value_si, delta=1e0) + self.assertAlmostEqual(fitted.n.value_si, answer.n.value_si, 1, 4) + self.assertAlmostEqual(fitted.Ea.value_si, answer.Ea.value_si, 2) + self.assertAlmostEqual(fitted.T0.value_si, answer.T0.value_si, 4) + + def test_to_arrhenius_temperature_range(self): + """ + Test the to_arrhenius temperature range is set correctly. + """ + answer = self.single_kinetics.arrhenius[0] + fitted = self.single_kinetics.to_arrhenius(Tmin=800, Tmax=1200) + self.assertAlmostEqual(fitted.Tmin.value_si, 800.0) + self.assertAlmostEqual(fitted.Tmax.value_si, 1200.0) + for T in [800, 1000, 1200]: + self.assertAlmostEqual(fitted.get_rate_coefficient(T) / answer.get_rate_coefficient(T), 1.0) + + def test_to_arrhenius_multiple(self): + """ + Test the to_arrhenius fitting multiple kinetics over a small range, see if we're within 5% at a few points + """ + answer = self.kinetics + fitted = self.kinetics.to_arrhenius(Tmin=800, Tmax=1200) + self.assertAlmostEqual(fitted.Tmin.value_si, 800.0) + self.assertAlmostEqual(fitted.Tmax.value_si, 1200.0) + for T in [800, 1000, 1200]: + self.assertAlmostEqual(fitted.get_rate_coefficient(T) / answer.get_rate_coefficient(T), 1.0, delta=0.05) + + def test_change_rate(self): + """ + Test the MultiArrhenius.change_rate() method. + """ + Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + k0list = np.array([self.kinetics.get_rate_coefficient(T) for T in Tlist]) + self.kinetics.change_rate(2) + for T, kexp in zip(Tlist, k0list): + kact = self.kinetics.get_rate_coefficient(T) + self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) + + +################################################################################ + +class TestMultiPDepArrhenius(unittest.TestCase): + """ + Contains unit tests of the :class:`MultiPDepArrhenius` class. + """ + + def setUp(self): + """ + A function run before each unit test in this class. + """ + self.Tmin = 350. + self.Tmax = 1500. + self.Pmin = 1e-1 + self.Pmax = 1e1 + self.pressures = np.array([1e-1, 1e1]) + self.comment = 'CH3 + C2H6 <=> CH4 + C2H5 (Baulch 2005)' + self.arrhenius = [ + PDepArrhenius( + pressures=(self.pressures, "bar"), + arrhenius=[ + Arrhenius( + A=(9.3e-16, "cm^3/(molecule*s)"), + n=0.0, + Ea=(4740 * constants.R * 0.001, "kJ/mol"), + T0=(1, "K"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ), + Arrhenius( + A=(9.3e-14, "cm^3/(molecule*s)"), + n=0.0, + Ea=(4740 * constants.R * 0.001, "kJ/mol"), + T0=(1, "K"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ), + ], + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + Pmin=(self.Pmin, "bar"), + Pmax=(self.Pmax, "bar"), + comment=self.comment, + ), + PDepArrhenius( + pressures=(self.pressures, "bar"), + arrhenius=[ + Arrhenius( + A=(1.4e-11, "cm^3/(molecule*s)"), + n=0.0, + Ea=(11200 * constants.R * 0.001, "kJ/mol"), + T0=(1, "K"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ), + Arrhenius( + A=(1.4e-9, "cm^3/(molecule*s)"), + n=0.0, + Ea=(11200 * constants.R * 0.001, "kJ/mol"), + T0=(1, "K"), + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + comment=self.comment, + ), + ], + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + Pmin=(self.Pmin, "bar"), + Pmax=(self.Pmax, "bar"), + comment=self.comment, + ), + ] + self.kinetics = MultiPDepArrhenius( + arrhenius=self.arrhenius, + Tmin=(self.Tmin, "K"), + Tmax=(self.Tmax, "K"), + Pmin=(self.Pmin, "bar"), + Pmax=(self.Pmax, "bar"), + comment=self.comment, + ) + + def test_arrhenius(self): + """ + Test that the MultiPDepArrhenius arrhenius property was properly set. + """ + self.assertEqual(self.kinetics.arrhenius, self.arrhenius) + + def test_temperature_min(self): + """ + Test that the MultiPDepArrhenius Tmin property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Tmin.value_si, self.Tmin, 6) + + def test_temperature_max(self): + """ + Test that the MultiPDepArrhenius Tmax property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Tmax.value_si, self.Tmax, 6) + + def test_pressure_min(self): + """ + Test that the MultiPDepArrhenius Pmin property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Pmin.value_si * 1e-5, self.Pmin, 6) + + def test_pressure_max(self): + """ + Test that the MultiPDepArrhenius Pmax property was properly set. + """ + self.assertAlmostEqual(self.kinetics.Pmax.value_si * 1e-5, self.Pmax, 6) + + def test_comment(self): + """ + Test that the MultiPDepArrhenius comment property was properly set. + """ + self.assertEqual(self.kinetics.comment, self.comment) + + def test_is_temperature_valid(self): + """ + Test the MultiPDepArrhenius.is_temperature_valid() method. + """ + Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + validdata = np.array([False, True, True, True, True, True, True, False, False, False], np.bool) + for T, valid in zip(Tdata, validdata): + valid0 = self.kinetics.is_temperature_valid(T) + self.assertEqual(valid0, valid) + + def test_is_pressure_valid(self): + """ + Test the MultiPDepArrhenius.is_pressure_valid() method. + """ + Pdata = np.array([1e3, 1e4, 1e5, 1e6, 1e7]) + validdata = np.array([False, True, True, True, False], np.bool) + for P, valid in zip(Pdata, validdata): + valid0 = self.kinetics.is_pressure_valid(P) + self.assertEqual(valid0, valid) + + def test_get_rate_coefficient(self): + """ + Test the MultiPDepArrhenius.get_rate_coefficient() method. + """ + Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + Plist = np.array([1e4, 1e5, 1e6]) + kexplist = np.array([ + [2.85400e-08, 4.00384e-03, 2.73563e-01, 8.50699e+00, 1.20181e+02, 7.56312e+02, 2.84724e+03, 7.71702e+03, + 1.67743e+04, 3.12290e+04], + [2.85400e-07, 4.00384e-02, 2.73563e+00, 8.50699e+01, 1.20181e+03, 7.56312e+03, 2.84724e+04, 7.71702e+04, + 1.67743e+05, 3.12290e+05], + [2.85400e-06, 4.00384e-01, 2.73563e+01, 8.50699e+02, 1.20181e+04, 7.56312e+04, 2.84724e+05, 7.71702e+05, + 1.67743e+06, 3.12290e+06], + ]).T + for i in range(Tlist.shape[0]): + for j in range(Plist.shape[0]): + kexp = kexplist[i, j] + kact = self.kinetics.get_rate_coefficient(Tlist[i], Plist[j]) + self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) + + def test_get_rate_coefficient_diff_plist(self): + """ + Test the MultiPDepArrhenius.get_rate_coefficient() when plists are different. + """ + # modify the MultiPDepArrhenius object with an additional entry + pressures = np.array([1e-1, 1e-1, 1e1]) + self.kinetics.arrhenius[0].pressures = (pressures, "bar") + self.kinetics.arrhenius[0].arrhenius.insert(0, self.kinetics.arrhenius[0].arrhenius[0]) + + Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) + Plist = np.array([1e4, 1e5, 1e6]) + kexplist = np.array([ + [2.85400e-08, 4.00384e-03, 2.73563e-01, 8.50699e+00, 1.20181e+02, 7.56312e+02, 2.84724e+03, 7.71702e+03, + 1.67743e+04, 3.12290e+04], + [2.85400e-07, 4.00384e-02, 2.73563e+00, 8.50699e+01, 1.20181e+03, 7.56312e+03, 2.84724e+04, 7.71702e+04, + 1.67743e+05, 3.12290e+05], + [2.85400e-06, 4.00384e-01, 2.73563e+01, 8.50699e+02, 1.20181e+04, 7.56312e+04, 2.84724e+05, 7.71702e+05, + 1.67743e+06, 3.12290e+06], + ]).T + for i in range(Tlist.shape[0]): + for j in range(Plist.shape[0]): + kexp = kexplist[i, j] + kact = self.kinetics.get_rate_coefficient(Tlist[i], Plist[j]) + self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) + + def test_pickle(self): + """ + Test that a MultiPDepArrhenius object can be pickled and unpickled with + no loss of information. + """ + import pickle + kinetics = pickle.loads(pickle.dumps(self.kinetics, -1)) + self.assertEqual(len(self.kinetics.arrhenius), len(kinetics.arrhenius)) + self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) + self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) + self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) + self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) + self.assertEqual(self.kinetics.comment, kinetics.comment) + + def test_repr(self): + """ + Test that a MultiPDepArrhenius object can be reconstructed from its + repr() output with no loss of information. + """ + namespace = {} + exec('kinetics = {0!r}'.format(self.kinetics), globals(), namespace) + self.assertIn('kinetics', namespace) + kinetics = namespace['kinetics'] + self.assertEqual(len(self.kinetics.arrhenius), len(kinetics.arrhenius)) + self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) + self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) + self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) + self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) + self.assertEqual(self.kinetics.comment, kinetics.comment) + + def test_change_rate(self): + """ + Test the PDepMultiArrhenius.change_rate() method. + """ + Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + k0list = np.array([self.kinetics.get_rate_coefficient(T, 1e5) for T in Tlist]) + self.kinetics.change_rate(2) + for T, kexp in zip(Tlist, k0list): + kact = self.kinetics.get_rate_coefficient(T, 1e5) + self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) + + def test_generate_reverse_rate_coefficient(self): + """ + Test ability to reverse a reaction rate. + + This is a real example from an imported chemkin file. + """ + from rmgpy.species import Species + from rmgpy.molecule import Molecule + from rmgpy.data.kinetics import LibraryReaction + from rmgpy.thermo import NASA, NASAPolynomial + test_reaction = LibraryReaction(reactants=[Species(label="C2H3", thermo=NASA(polynomials=[NASAPolynomial(coeffs=[3.12502,0.00235137,2.36803e-05,-3.35092e-08,1.39444e-11,34524.3,8.81538], Tmin=(200,"K"), Tmax=(1000,"K")), NASAPolynomial(coeffs=[4.37211,0.00746869,-2.64716e-06,4.22753e-10,-2.44958e-14,33805.2,0.428772], Tmin=(1000,"K"), Tmax=(6000,"K"))], Tmin=(200,"K"), Tmax=(6000,"K"), E0=(285.696,"kJ/mol"), Cp0=(33.2579,"J/mol/K"), CpInf=(108.088,"J/mol/K"), comment="""ATcT3E\nC2H3 ATcT ver. 1.122, DHf298 = 296.91 ± 0.33 kJ/mol - fit JAN17"""), molecule=[Molecule(smiles="[CH]=C")], molecular_weight=(27.0452,"amu")), + Species(label="CH2O", thermo=NASA(polynomials=[NASAPolynomial(coeffs=[4.77187,-0.00976266,3.70122e-05,-3.76922e-08,1.31327e-11,-14379.8,0.696586], Tmin=(200,"K"), Tmax=(1000,"K")), NASAPolynomial(coeffs=[2.91333,0.0067004,-2.55521e-06,4.27795e-10,-2.44073e-14,-14462.2,7.43823], Tmin=(1000,"K"), Tmax=(6000,"K"))], Tmin=(200,"K"), Tmax=(6000,"K"), E0=(-119.527,"kJ/mol"), Cp0=(33.2579,"J/mol/K"), CpInf=(83.1447,"J/mol/K"), comment="""ATcT3E\nH2CO ATcT ver. 1.122, DHf298 = -109.188 ± 0.099 kJ/mol - fit JAN17"""), molecule=[Molecule(smiles="C=O")], molecular_weight=(30.026,"amu"))], + products=[Species(label="C2H4", thermo=NASA(polynomials=[NASAPolynomial(coeffs=[3.65151,-0.00535067,5.16486e-05,-6.36869e-08,2.50743e-11,5114.51,5.38561], Tmin=(200,"K"), Tmax=(1000,"K")), NASAPolynomial(coeffs=[4.14446,0.0102648,-3.61247e-06,5.74009e-10,-3.39296e-14,4190.59,-1.14778], Tmin=(1000,"K"), Tmax=(6000,"K"))], Tmin=(200,"K"), Tmax=(6000,"K"), E0=(42.06,"kJ/mol"), Cp0=(33.2579,"J/mol/K"), CpInf=(133.032,"J/mol/K"), comment="""ATcT3E\nC2H4 ATcT ver. 1.122, DHf298 = 52.45 ± 0.13 kJ/mol - fit JAN17"""), molecule=[Molecule(smiles="C=C")], molecular_weight=(28.0532,"amu")), + Species(label="HCO", thermo=NASA(polynomials=[NASAPolynomial(coeffs=[3.97075,-0.00149122,9.54042e-06,-8.8272e-09,2.67645e-12,3842.03,4.4466], Tmin=(200,"K"), Tmax=(1000,"K")), NASAPolynomial(coeffs=[3.85781,0.00264114,-7.44177e-07,1.23313e-10,-8.88959e-15,3616.43,3.92451], Tmin=(1000,"K"), Tmax=(6000,"K"))], Tmin=(200,"K"), Tmax=(6000,"K"), E0=(32.0237,"kJ/mol"), Cp0=(33.2579,"J/mol/K"), CpInf=(58.2013,"J/mol/K"), comment="""HCO ATcT ver. 1.122, DHf298 = 41.803 ± 0.099 kJ/mol - fit JAN17"""), molecule=[Molecule(smiles="[CH]=O")], molecular_weight=(29.018,"amu"))], + kinetics=MultiPDepArrhenius(arrhenius=[PDepArrhenius(pressures=([0.001,0.01,0.1,1,10,100,1000],"atm"), + arrhenius=[Arrhenius(A=(1.1e+07,"cm^3/(mol*s)"), n=1.09, Ea=(1807,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(2.5e+07,"cm^3/(mol*s)"), n=0.993, Ea=(1995,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(2.5e+08,"cm^3/(mol*s)"), n=0.704, Ea=(2596,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(1.4e+10,"cm^3/(mol*s)"), n=0.209, Ea=(3934,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(3.5e+13,"cm^3/(mol*s)"), n=-0.726, Ea=(6944,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(3.3e+14,"cm^3/(mol*s)"), n=-0.866, Ea=(10966,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(17,"cm^3/(mol*s)"), n=3.17, Ea=(9400,"cal/mol"), T0=(1,"K"))]), + PDepArrhenius(pressures=([0.001,0.01,0.1,1,10,100,1000],"atm"), + arrhenius=[Arrhenius(A=(-2.3e+16,"cm^3/(mol*s)"), n=-1.269, Ea=(20617,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(-5.2e+16,"cm^3/(mol*s)"), n=-1.366, Ea=(20805,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(-1.5e+18,"cm^3/(mol*s)"), n=-1.769, Ea=(22524,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(-8.5e+19,"cm^3/(mol*s)"), n=-2.264, Ea=(23862,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(-4.4e+23,"cm^3/(mol*s)"), n=-3.278, Ea=(27795,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(-4.2e+24,"cm^3/(mol*s)"), n=-3.418, Ea=(31817,"cal/mol"), T0=(1,"K")), + Arrhenius(A=(-2.1e+11,"cm^3/(mol*s)"), n=0.618, Ea=(30251,"cal/mol"), T0=(1,"K"))]) + ]), duplicate=True) + test_reaction.generate_reverse_rate_coefficient() From d1dd0fb087a5903a504beb25095cab8e94c42761 Mon Sep 17 00:00:00 2001 From: Richard West Date: Mon, 17 Jul 2023 23:20:34 -0400 Subject: [PATCH 064/109] Fix ArrheniusBM.get_activation_energy unit test. The E0 had been changed, but the expected Ea not updated. --- rmgpy/kinetics/arrheniusTest.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rmgpy/kinetics/arrheniusTest.py b/rmgpy/kinetics/arrheniusTest.py index d7d4f72974..d61d8b4b27 100644 --- a/rmgpy/kinetics/arrheniusTest.py +++ b/rmgpy/kinetics/arrheniusTest.py @@ -528,7 +528,13 @@ def test_get_activation_energy(self): """ Hrxn = -44000 # J/mol Ea = self.arrhenius_bm.get_activation_energy(Hrxn) - self.assertAlmostEqual(Ea, 95074, delta=1e1) + + w = self.w0 + E0 = self.E0 + Vp = 2 * w * (w + E0)/(w - E0) + Ea_exp = (w + Hrxn/2) * (Vp - 2*w + Hrxn)**2 / (Vp*Vp - 4*w*w + Hrxn*Hrxn) + + self.assertAlmostEqual(Ea, Ea_exp, delta=1e1) ################################################################################ From 2e41f0133c3bc2c62e577ba310009c618d055106 Mon Sep 17 00:00:00 2001 From: davidfarinajr Date: Tue, 18 Jan 2022 11:39:04 -0500 Subject: [PATCH 065/109] modifed arrbm `fit_to_data` unit test Test now compares the fitted rate to the rate it was trained on to make sure they agree. --- rmgpy/kinetics/arrheniusTest.py | 60 ++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/rmgpy/kinetics/arrheniusTest.py b/rmgpy/kinetics/arrheniusTest.py index d61d8b4b27..e3fdff52bf 100644 --- a/rmgpy/kinetics/arrheniusTest.py +++ b/rmgpy/kinetics/arrheniusTest.py @@ -41,7 +41,7 @@ from rmgpy.molecule.molecule import Molecule from rmgpy.reaction import Reaction from rmgpy.species import Species -from rmgpy.thermo import NASA, NASAPolynomial +from rmgpy.thermo import NASA, NASAPolynomial, ThermoData ################################################################################ @@ -460,6 +460,42 @@ def setUp(self): comment="""Thermo library: Spiekermann_refining_elementary_reactions""" ) + CF2 = Species().from_adjacency_list( + """ + 1 F u0 p3 c0 {2,S} + 2 C u0 p1 c0 {1,S} {3,S} + 3 F u0 p3 c0 {2,S} + """ + ) + CF2.thermo = NASA( + polynomials=[ + NASAPolynomial(coeffs=[2.28591,0.0107608,-1.05382e-05,4.89881e-09,-8.86384e-13,-24340.7,13.1348], Tmin=(298,'K'), Tmax=(1300,'K')), + NASAPolynomial(coeffs=[5.33121,0.00197748,-9.60248e-07,2.10704e-10,-1.5954e-14,-25190.9,-2.56367], Tmin=(1300,'K'), Tmax=(3000,'K')) + ], + Tmin=(298,'K'), Tmax=(3000,'K'), Cp0=(33.2579,'J/mol/K'), CpInf=(58.2013,'J/mol/K'), + comment="""Thermo library: halogens""" + ) + C2H6 = Species(smiles="CC") + C2H6.thermo = ThermoData( + Tdata = ([300,400,500,600,800,1000,1500],'K'), + Cpdata = ([12.565,15.512,18.421,21.059,25.487,28.964,34.591],'cal/(mol*K)','+|-',[0.8,1.1,1.3,1.4,1.5,1.5,1.2]), + H298 = (-20.028,'kcal/mol','+|-',0.1), + S298 = (54.726,'cal/(mol*K)','+|-',0.6), + comment="""Thermo library: DFT_QCI_thermo""" + ) + CH3CF2CH3 = Species(smiles="CC(F)(F)C") + CH3CF2CH3.thermo = NASA( + polynomials = [ + NASAPolynomial(coeffs=[3.89769,0.00706735,0.000140168,-3.37628e-07,2.51812e-10,-68682.1,8.74321], Tmin=(10,'K'), Tmax=(436.522,'K')), + NASAPolynomial(coeffs=[2.78849,0.0356982,-2.16715e-05,6.45057e-09,-7.47989e-13,-68761.2,11.1597], Tmin=(436.522,'K'), Tmax=(3000,'K')), + ], + Tmin = (10,'K'), Tmax = (3000,'K'), Cp0 = (33.2579,'J/(mol*K)'), CpInf = (249.434,'J/(mol*K)'), + comment="""Thermo library: CHOF_G4""" + ) + kinetics = Arrhenius(A=(0.222791,'cm^3/(mol*s)'), n=3.59921, Ea=(320.496,'kJ/mol'), T0=(1,'K'), Tmin=(298,'K'), Tmax=(2500,'K'), comment="""Training Rxn 54 for 1,2_Insertion_carbene""") + self.reaction = Reaction(reactants=[CF2,C2H6],products=[CH3CF2CH3],kinetics=kinetics) + self.reaction_w0 = 519000 # J/mol + def test_a_factor(self): """ Test that the ArrheniusBM A property was properly set. @@ -510,17 +546,26 @@ def test_fit_to_data(self): """ Test the ArrheniusBM.fit_to_data() method. """ + Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + reactant = Molecule(smiles=self.rsmi) product = Molecule(smiles=self.psmi) reaction = Reaction(reactants=[Species(molecule=[reactant], thermo=self.r_thermo,)], products=[Species(molecule=[product], thermo=self.p_thermo)], kinetics=self.arrhenius, ) - + kdata = np.array([reaction.kinetics.get_rate_coefficient(T) for T in Tdata]) arrhenius_bm = ArrheniusBM().fit_to_reactions([reaction], w0=self.w0) - self.assertAlmostEqual(arrhenius_bm.A.value_si, self.arrhenius_bm.A.value_si, delta=1.5e1) - self.assertAlmostEqual(arrhenius_bm.n.value_si, self.arrhenius_bm.n.value_si, 1, 4) - self.assertAlmostEqual(arrhenius_bm.E0.value_si, self.arrhenius_bm.E0.value_si, 1) + arrhenius = arrhenius_bm.to_arrhenius(reaction.get_enthalpy_of_reaction(298)) + for T, k in zip(Tdata, kdata): + self.assertAlmostEqual(k, arrhenius.get_rate_coefficient(T), delta=1e-6 * k) + + # A second check, with a different reaction + arrhenius_bm = ArrheniusBM().fit_to_reactions([self.reaction], w0=self.reaction_w0) + arrhenius = arrhenius_bm.to_arrhenius(self.reaction.get_enthalpy_of_reaction(298)) + kdata = np.array([self.reaction.kinetics.get_rate_coefficient(T) for T in Tdata]) + for T, k in zip(Tdata, kdata): + self.assertAlmostEqual(k, arrhenius.get_rate_coefficient(T), delta=1e-6 * k) def test_get_activation_energy(self): """ @@ -1201,3 +1246,8 @@ def test_generate_reverse_rate_coefficient(self): Arrhenius(A=(-2.1e+11,"cm^3/(mol*s)"), n=0.618, Ea=(30251,"cal/mol"), T0=(1,"K"))]) ]), duplicate=True) test_reaction.generate_reverse_rate_coefficient() + +################################################################################ + +if __name__ == '__main__': + unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) From de545c03364ea55ea83c893dd0659c661e0fa923 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 7 May 2023 14:26:07 -0700 Subject: [PATCH 066/109] handle charge properly in fragment smiles hack --- rmgpy/molecule/fragment.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rmgpy/molecule/fragment.py b/rmgpy/molecule/fragment.py index 8408c82b6f..2df73ae832 100644 --- a/rmgpy/molecule/fragment.py +++ b/rmgpy/molecule/fragment.py @@ -660,9 +660,8 @@ def to_smiles(self): substi = Atom( element=get_element("Si"), radical_electrons=0, - charge=0, - lone_pairs=3, - ) + charge=-3, + lone_pairs=3) substi.label = element_symbol for bonded_atom, bond in atom.edges.items(): From 70659ab1b078da96f7eb6ed8b4c6c09161e0e7f0 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 7 May 2023 14:26:38 -0700 Subject: [PATCH 067/109] skip nodes that are empty when pulling solute data --- rmgpy/data/kinetics/family.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 42b1cfcba7..81d20d29fc 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -3565,6 +3565,8 @@ def make_bm_rules_from_template_rxn_map(self, template_rxn_map, nprocs=1, Tref=1 index += 1 for label,entry in self.rules.entries.items(): #pull solute data from further up the tree as needed + if len(entry) == 0: + continue entry = entry[0] if not entry.data.solute: ent = self.groups.entries[label] From 3faaf7f5bb78eb002e58f4f5280b07cab2848e4b Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Mon, 8 May 2023 22:47:34 -0700 Subject: [PATCH 068/109] update product template after generation --- rmgpy/data/kinetics/family.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 81d20d29fc..bb9da51ef4 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -658,6 +658,9 @@ def load(self, path, local_context=None, global_context=None, depository_labels= self.reverse = local_context.get('reverse', None) self.reversible = True if local_context.get('reversible', None) is None else local_context.get('reversible', None) self.forward_template.products = self.generate_product_template(self.forward_template.reactants) + for entry in self.forward_template.products: + if isinstance(entry.item,Group): + entry.item.update() if self.reversible: self.reverse_template = Reaction(reactants=self.forward_template.products, products=self.forward_template.reactants) From aecd710eeb404006d2060ae7df8cd194462c4b55 Mon Sep 17 00:00:00 2001 From: ssun30 Date: Wed, 6 Nov 2024 11:29:56 -0500 Subject: [PATCH 069/109] add Li adsorption to test data --- .../thermo/groups/adsorptionLi.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/rmgpy/test_data/testing_database/thermo/groups/adsorptionLi.py diff --git a/test/rmgpy/test_data/testing_database/thermo/groups/adsorptionLi.py b/test/rmgpy/test_data/testing_database/thermo/groups/adsorptionLi.py new file mode 100644 index 0000000000..4b47339761 --- /dev/null +++ b/test/rmgpy/test_data/testing_database/thermo/groups/adsorptionLi.py @@ -0,0 +1,52 @@ +name = "Surface Adsorption Corrections" +shortDesc = "" +longDesc = """ +Changes due to adsorbing on a surface. +Here, Pt(111) +Note: "-h" means "horizontal". +""" + +entry( + index = 1, + label = "R*", + group= +""" +1 R u0 +2 X u0 +""", + thermo=None, + shortDesc="""Anything adsorbed anyhow.""", + longDesc=""" + R + x +*********** +This node should be empty, ensuring that one of the nodes below is used. +""", + metal = "Pt", + facet = "111", +) + +entry( + index = 1, + label = "R-*", + group = +""" +1 X u0 p0 c0 {2,S} +2 R u0 p0 c0 {1,S} +""", + thermo=ThermoData( + Tdata=([300, 400, 500, 600, 800, 1000, 1500], 'K'), + Cpdata=([-3.01, -1.78, -0.96, -0.41, 0.23, 0.56, 0.91], 'cal/(mol*K)'), + H298=(-86.29, 'kcal/mol'), + S298=(-26.39, 'cal/(mol*K)'), + ), + shortDesc="""Came from H single-bonded on Pt(111)""", + longDesc="""Calculated by Katrin Blondal at Brown University using statistical mechanics (files: compute_NASA_for_Pt-adsorbates.ipynb and compute_NASA_for_Pt-gas_phase.ipynb). Based on DFT calculations by Jelena Jelic at KIT. + + R + | +*********** +""", + metal = "Pt", + facet = "111", +) From 79fc764406d14b8bcaf12edc67f5a170b8c17846 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 3 Jun 2023 16:44:37 -0700 Subject: [PATCH 070/109] don't check collision limit for reactions without kinetics --- test/database/databaseTest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/database/databaseTest.py b/test/database/databaseTest.py index 3011ba17eb..4a6c73a45c 100644 --- a/test/database/databaseTest.py +++ b/test/database/databaseTest.py @@ -50,6 +50,7 @@ from rmgpy.molecule.atomtype import ATOMTYPES from rmgpy.molecule.pathfinder import find_shortest_path from rmgpy.quantity import ScalarQuantity +from rmgpy.kinetics.model import KineticsModel # allow asserts to 'fail' and then continue - this test file relies on a lot # of asserts in each test and we want them all to run @@ -970,9 +971,10 @@ def kinetics_check_library_rates_are_reasonable(self, library): tst_limit = (kB * T) / h collision_limit = Na * np.pi * h_rad_diam**2 * np.sqrt(8 * kB * T / (np.pi * h_rad_mass / 2)) for entry in library.entries.values(): - if entry.item.is_surface_reaction(): + if entry.item.is_surface_reaction() or isinstance(entry.data, KineticsModel): # Don't check surface reactions continue + k = entry.data.get_rate_coefficient(T, P) rxn = entry.item if k < 0: From 1a7175d49bd175019613692546ca354abc2590b7 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 10 Jun 2023 15:15:35 -0700 Subject: [PATCH 071/109] fix charge handling in make sample molecule checks that the first group_atom.atomtype is either an appropriate charged atomtype or is a general version of an appropriate charged atomtype --- rmgpy/molecule/group.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rmgpy/molecule/group.py b/rmgpy/molecule/group.py index 9cb2297636..e445f0ce24 100644 --- a/rmgpy/molecule/group.py +++ b/rmgpy/molecule/group.py @@ -1013,7 +1013,7 @@ def is_van_der_waals(self, wildcards=False): else: return abs(self.order[0]) <= 1e-9 and len(self.order) == 1 - + def is_reaction_bond(self, wildcards=False): """ Return ``True`` if the bond represents a van der Waals bond or ``False`` if @@ -1851,7 +1851,7 @@ def specify_bond_extensions(self, i, j, basename, r_bonds): else: atom_type_j_str = atom_type_j[0].label - b = None + b = None for v in bdict.keys(): if abs(v - bd) < 1e-4: b = bdict[v] @@ -2993,9 +2993,11 @@ def make_sample_molecule(self): 'O0sc', 'P0sc', 'P1sc', 'P1dc', 'P5sc', 'S0sc', 'S2sc', 'S2dc', 'S2tc', 'S4sc', 'S4dc', 'S4tdc', 'S6sc', 'S6dc', 'S6tdc'] - if group_atom.atomtype[0] in [ATOMTYPES[x] for x in positive_charged] and atom.charge > 0: + if atom.charge > 0 and any([group_atom.atomtype[0] is ATOMTYPES[x] or ATOMTYPES[x].is_specific_case_of(group_atom.atomtype[0]) for x in positive_charged]): + pass + elif atom.charge < 0 and any([group_atom.atomtype[0] is ATOMTYPES[x] or ATOMTYPES[x].is_specific_case_of(group_atom.atomtype[0]) for x in negative_charged]): pass - elif atom.charge in group_atom.charge: + elif atom.charge in group_atom.atomtype[0].charge: # declared charge in original group is same as new charge pass else: From aa88c7e23e16d4426bb74915a1c668b08f16ae49 Mon Sep 17 00:00:00 2001 From: Richard West Date: Mon, 17 Jul 2023 17:39:23 -0400 Subject: [PATCH 072/109] Refactor some rate fitting in rule generation. It was:: if n > 1: # big long block else: return None I inverted the check so we can return early, and outdent a huge block of code. --- rmgpy/data/kinetics/family.py | 139 ++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 66 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index bb9da51ef4..12fdeb2350 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4542,7 +4542,14 @@ def get_objective_function(kinetics1, kinetics2, obj=information_gain, T=1000.0) def _make_rule(rr): """ - function for parallelization of rule and uncertainty calculation + Function for parallelization of rule and uncertainty calculation + + Input: rr - tuple of (recipe, rxns, Tref, fmax, label, ranks) + rxns and ranks are lists of equal length. + Output: kinetics object, with uncertainty and comment attached. + If Blowers-Masel fitting is successful it will be ArrheniusBM or ArrheniusChargeTransferBM, + else Arrhenius, SurfaceChargeTransfer, or ArrheniusChargeTransfer. + Errors in Ln(k) at each reaction are treated as samples from a weighted normal distribution weights are inverse variance weights based on estimates of the error in Ln(k) for each individual reaction """ @@ -4550,77 +4557,77 @@ def _make_rule(rr): for i, rxn in enumerate(rxns): rxn.rank = ranks[i] rxns = np.array(rxns) - rs = np.array([r for r in rxns if type(r.kinetics) != KineticsModel]) + rs = np.array([r for r in rxns if type(r.kinetics) != KineticsModel]) # KineticsModel is the base class with no data. n = len(rs) data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rs])) - if n > 0: - if isinstance(rs[0].kinetics, Arrhenius): - arr = ArrheniusBM + + if n == 0: + return None + + if isinstance(rs[0].kinetics, Arrhenius): + arr = ArrheniusBM + else: + arr = ArrheniusChargeTransferBM + if n > 1: + kin = arr().fit_to_reactions(rs, recipe=recipe) + if n == 1 or kin.E0.value_si < 0.0: + kin = average_kinetics([r.kinetics for r in rs]) + #kin.comment = "Only one reaction or Arrhenius BM fit bad. Instead averaged from {} reactions.".format(n) + if n == 1: + kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) else: - arr = ArrheniusChargeTransferBM - if n > 1: - kin = arr().fit_to_reactions(rs, recipe=recipe) - if n == 1 or kin.E0.value_si < 0.0: - kin = average_kinetics([r.kinetics for r in rs]) - #kin.comment = "Only one reaction or Arrhenius BM fit bad. Instead averaged from {} reactions.".format(n) - if n == 1: - kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) - else: + dlnks = np.array([ + np.log( + average_kinetics([r.kinetics for r in rs[list(set(range(len(rs))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + else: + if n == 1: + kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + else: + if isinstance(rs[0].kinetics, Arrhenius): dlnks = np.array([ np.log( - average_kinetics([r.kinetics for r in rs[list(set(range(len(rs))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rs) - ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 - # weighted average calculations - ws = 1.0 / varis - V1 = ws.sum() - V2 = (ws ** 2).sum() - mu = np.dot(ws, dlnks) / V1 - s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) - kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) - else: - if n == 1: - kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref else: - if isinstance(rs[0].kinetics, Arrhenius): - dlnks = np.array([ - np.log( - arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) - .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) - .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rs) - ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - else: - dlnks = np.array([ - np.log( - arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) - .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) - .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rs) - ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 - # weighted average calculations - ws = 1.0 / varis - V1 = ws.sum() - V2 = (ws ** 2).sum() - mu = np.dot(ws, dlnks) / V1 - s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) - kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) - - #site solute parameters - site_datas = [get_site_solute_data(rxn) for rxn in rxns] - site_datas = [sdata for sdata in site_datas if sdata is not None] - if len(site_datas) > 0: - site_data = SoluteTSData() - for sdata in site_datas: - site_data += sdata - site_data = site_data * (1.0/len(site_datas)) - kin.solute = site_data - return kin - else: - return None - + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + + #site solute parameters + site_datas = [get_site_solute_data(rxn) for rxn in rxns] + site_datas = [sdata for sdata in site_datas if sdata is not None] + if len(site_datas) > 0: + site_data = SoluteTSData() + for sdata in site_datas: + site_data += sdata + site_data = site_data * (1.0/len(site_datas)) + kin.solute = site_data + return kin def _spawn_tree_process(family, template_rxn_map, obj, T, nprocs, depth, min_splitable_entry_num, min_rxns_to_spawn, extension_iter_max, extension_iter_item_cap): parent_conn, child_conn = mp.Pipe() From 4dad2b72b19232ebd860fbbdbb563b51fe8264d6 Mon Sep 17 00:00:00 2001 From: Richard West Date: Mon, 17 Jul 2023 21:23:42 -0400 Subject: [PATCH 073/109] Minor refactor. --- rmgpy/data/kinetics/family.py | 57 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 12fdeb2350..8efd8cd56f 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4571,11 +4571,13 @@ def _make_rule(rr): if n > 1: kin = arr().fit_to_reactions(rs, recipe=recipe) if n == 1 or kin.E0.value_si < 0.0: + # still run it through the averaging function when n=1 to standardize the units and run checks kin = average_kinetics([r.kinetics for r in rs]) - #kin.comment = "Only one reaction or Arrhenius BM fit bad. Instead averaged from {} reactions.".format(n) if n == 1: kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + kin.comment = f"Only one reaction rate: {rs[0]!s}" else: + kin.comment = f"Blowers-Masel fit was bad (E0<0) so instead averaged from {n} reactions." dlnks = np.array([ np.log( average_kinetics([r.kinetics for r in rs[list(set(range(len(rs))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) @@ -4589,34 +4591,31 @@ def _make_rule(rr): mu = np.dot(ws, dlnks) / V1 s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) - else: - if n == 1: - kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) - else: - if isinstance(rs[0].kinetics, Arrhenius): - dlnks = np.array([ - np.log( - arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) - .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) - .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rs) - ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - else: - dlnks = np.array([ - np.log( - arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) - .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) - .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rs) - ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 - # weighted average calculations - ws = 1.0 / varis - V1 = ws.sum() - V2 = (ws ** 2).sum() - mu = np.dot(ws, dlnks) / V1 - s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) - kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + else: # Blowers-Masel fit was good + if isinstance(rs[0].kinetics, Arrhenius): + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + else: # SurfaceChargeTransfer or ArrheniusChargeTransfer + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) #site solute parameters site_datas = [get_site_solute_data(rxn) for rxn in rxns] From b187f357f7cfaaed666303bf8f2b862a753120e7 Mon Sep 17 00:00:00 2001 From: Richard West Date: Tue, 18 Jul 2023 17:00:29 -0400 Subject: [PATCH 074/109] Consistently use T=298. K for fitting and evaluating Blowers-Masel rates. Closes https://github.com/ReactionMechanismGenerator/RMG-Py/issues/1748 --- rmgpy/data/kinetics/family.py | 8 ++++---- rmgpy/kinetics/arrhenius.pyx | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 8efd8cd56f..d4c55aa011 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -3658,13 +3658,13 @@ def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1 if kinetics.E0.value_si < 0.0 or len(L) == 1: kinetics = average_kinetics([r.kinetics for r in L]) else: - kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(298.0)) + kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(298.)) else: kinetics = ArrheniusChargeTransferBM().fit_to_reactions(L, recipe=self.forward_recipe.actions) if kinetics.E0.value_si < 0.0 or len(L) == 1: kinetics = average_kinetics([r.kinetics for r in L]) else: - kinetics = kinetics.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(298.0)) + kinetics = kinetics.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(298.)) k = kinetics.get_rate_coefficient(T) errors[rxn] = np.log(k / krxn) @@ -4596,7 +4596,7 @@ def _make_rule(rr): dlnks = np.array([ np.log( arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) - .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) + .to_arrhenius(rxn.get_enthalpy_of_reaction(298.)) .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) ) for i, rxn in enumerate(rs) ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref @@ -4604,7 +4604,7 @@ def _make_rule(rr): dlnks = np.array([ np.log( arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) - .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) + .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(298.)) .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) ) for i, rxn in enumerate(rs) ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref diff --git a/rmgpy/kinetics/arrhenius.pyx b/rmgpy/kinetics/arrhenius.pyx index 19e3221acc..20cef71120 100644 --- a/rmgpy/kinetics/arrhenius.pyx +++ b/rmgpy/kinetics/arrhenius.pyx @@ -559,7 +559,7 @@ cdef class ArrheniusBM(KineticsModel): """ Return the rate coefficient in the appropriate combination of m^3, mol, and s at temperature `T` in K and enthalpy of reaction `dHrxn` - in J/mol. + in J/mol, evaluated at 298 K. """ cdef double A, n, Ea Ea = self.get_activation_energy(dHrxn) @@ -570,7 +570,7 @@ cdef class ArrheniusBM(KineticsModel): cpdef double get_activation_energy(self, double dHrxn) except -1: """ Return the activation energy in J/mol corresponding to the given - enthalpy of reaction `dHrxn` in J/mol. + enthalpy of reaction `dHrxn` in J/mol, evaluated at 298 K. """ cdef double w0, E0 E0 = self._E0.value_si @@ -586,7 +586,8 @@ cdef class ArrheniusBM(KineticsModel): cpdef Arrhenius to_arrhenius(self, double dHrxn): """ Return an :class:`Arrhenius` instance of the kinetics model using the - given enthalpy of reaction `dHrxn` to determine the activation energy. + given enthalpy of reaction `dHrxn` (in J/mol, evaluated at 298 K) + to determine the activation energy. """ return Arrhenius( A=self.A, @@ -615,7 +616,6 @@ cdef class ArrheniusBM(KineticsModel): w0 = sum(w0s) / len(w0s) if len(rxns) == 1: - T = 1000.0 rxn = rxns[0] dHrxn = rxn.get_enthalpy_of_reaction(298.0) A = rxn.kinetics.A.value_si @@ -632,7 +632,7 @@ cdef class ArrheniusBM(KineticsModel): self.Tmin = rxn.kinetics.Tmin self.Tmax = rxn.kinetics.Tmax self.solute = None - self.comment = 'Fitted to {0} reaction at temperature: {1} K'.format(len(rxns), T) + self.comment = 'Fitted to 1 reaction.' else: # define optimization function def kfcn(xs, lnA, n, E0): From 3e3316e2ee89212d175341b4c36f198f01b8ea63 Mon Sep 17 00:00:00 2001 From: Richard West Date: Tue, 18 Jul 2023 17:01:42 -0400 Subject: [PATCH 075/109] Fixes and tweaks to Blowers Masel classes' fit_to_reactions methods. --- rmgpy/kinetics/arrhenius.pyx | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/rmgpy/kinetics/arrhenius.pyx b/rmgpy/kinetics/arrhenius.pyx index 20cef71120..fc8c69937a 100644 --- a/rmgpy/kinetics/arrhenius.pyx +++ b/rmgpy/kinetics/arrhenius.pyx @@ -605,6 +605,9 @@ cdef class ArrheniusBM(KineticsModel): """ Fit an ArrheniusBM model to a list of reactions at the given temperatures, w0 must be either given or estimated using the family object + + WARNING: there's a lot of code duplication with ArrheniusChargeTransferBM.fit_to_reactions + so anything you change here you should probably change there too and vice versa! """ assert w0 is not None or recipe is not None, 'either w0 or recipe must be specified' @@ -654,23 +657,22 @@ cdef class ArrheniusBM(KineticsModel): for T in Ts: xdata.append([T, rxn.get_enthalpy_of_reaction(298.0)]) ydata.append(np.log(rxn.get_rate_coefficient(T))) - sigmas.append(s / (8.314 * T)) xdata = np.array(xdata) ydata = np.array(ydata) # fit parameters - boo = True + keep_trying = True xtol = 1e-8 ftol = 1e-8 - while boo: - boo = False + while keep_trying: + keep_trying = False try: params = curve_fit(kfcn, xdata, ydata, sigma=sigmas, p0=[1.0, 1.0, w0 / 10.0], xtol=xtol, ftol=ftol) except RuntimeError: if xtol < 1.0: - boo = True + keep_trying = True xtol *= 10.0 ftol *= 10.0 else: @@ -687,6 +689,8 @@ cdef class ArrheniusBM(KineticsModel): # fill in parameters A_units = ['', 's^-1', 'm^3/(mol*s)', 'm^6/(mol^2*s)'] order = len(rxns[0].reactants) + if order != 1 and rxn.is_surface_reaction(): + raise NotImplementedError("Units not implemented for surface reactions.") self.A = (A, A_units[order]) self.n = n @@ -1534,8 +1538,11 @@ cdef class ArrheniusChargeTransferBM(KineticsModel): def fit_to_reactions(self, rxns, w0=None, recipe=None, Ts=None): """ - Fit an ArrheniusBM model to a list of reactions at the given temperatures, + Fit an ArrheniusChargeTransferBM model to a list of reactions at the given temperatures, w0 must be either given or estimated using the family object + + WARNING: there's a lot of code duplication with ArrheniusBM.fit_to_reactions + so anything you change here you should probably change there too and vice versa! """ assert w0 is not None or recipe is not None, 'either w0 or recipe must be specified' @@ -1588,23 +1595,22 @@ cdef class ArrheniusChargeTransferBM(KineticsModel): for T in Ts: xdata.append([T, rxn.get_enthalpy_of_reaction(298.0)]) ydata.append(np.log(rxn.get_rate_coefficient(T))) - sigmas.append(s / (8.314 * T)) xdata = np.array(xdata) ydata = np.array(ydata) # fit parameters - boo = True + keep_trying = True xtol = 1e-8 ftol = 1e-8 - while boo: - boo = False + while keep_trying: + keep_trying = False try: params = curve_fit(kfcn, xdata, ydata, sigma=sigmas, p0=[1.0, 1.0, w0 / 10.0], xtol=xtol, ftol=ftol) except RuntimeError: if xtol < 1.0: - boo = True + keep_trying = True xtol *= 10.0 ftol *= 10.0 else: @@ -1620,6 +1626,8 @@ cdef class ArrheniusChargeTransferBM(KineticsModel): # fill in parameters A_units = ['', 's^-1', 'm^3/(mol*s)', 'm^6/(mol^2*s)'] order = len(rxns[0].reactants) + if order != 1 and rxn.is_surface_reaction(): + raise NotImplementedError("Units not implemented for surface reactions") self.A = (A, A_units[order]) self.n = n From 236f4bc734287757ae8d603e00e2dd4340301aa4 Mon Sep 17 00:00:00 2001 From: Jackson Burns <33505528+JacksonBurns@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:50:46 -0400 Subject: [PATCH 076/109] allow `cat` of regression diff to fail, print a warning instead this will allow the regression test results to all be reported even if there is such a huge difference between the dynamic and baseline that the system utilities cannot print it see: https://github.com/ReactionMechanismGenerator/RMG-Py/pull/2316#issuecomment-1654884245 --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5183002095..19ae1ea7c7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -296,7 +296,7 @@ jobs: export FAILED=Yes fi echo "" # blank line so next block is interpreted as markdown - cat "$regr_test-core.log" + cat "$regr_test-core.log" || (echo "Dumping the whole log failed, please download it from GitHub actions. Here are the first 100 lines:" && head -n100 "$regr_test-core.log") echo "" echo "
" if python-jl scripts/checkModels.py \ @@ -313,7 +313,7 @@ jobs: export FAILED=Yes fi echo "" # blank line so next block is interpreted as markdown - cat "$regr_test-edge.log" + cat "$regr_test-edge.log" || (echo "Dumping the whole log failed, please download it from GitHub actions. Here are the first 100 lines:" && head -n100 "$regr_test-core.log") echo "
" # Check for Regression between Reference and Dynamic (skip superminimal) From 699143e40f7e2c251346f0271cc0edd70604ef88 Mon Sep 17 00:00:00 2001 From: Jackson Burns Date: Thu, 1 Feb 2024 10:37:49 -0500 Subject: [PATCH 077/109] combine the messy Cython declarations (and remove dupes) in reaction.py --- rmgpy/reaction.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index 9de4bb7ad2..f8836ba020 100644 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -731,13 +731,23 @@ def get_equilibrium_constant(self, T, potential=0., type='Kc', surface_site_dens surface species, the `surface_site_density` is the assumed reference. For protons (H+), a reference concentration of 1000 mol/m^3 (1 mol/L) is assumed """ - cython.declare(dGrxn=cython.double, K=cython.double, C0=cython.double, P0=cython.double) - cython.declare(dN_gas=cython.int, dN_surf=cython.int, dGrxn=cython.double, K=cython.double, C0=cython.double, P0=cython.double) - cython.declare(number_of_gas_reactants=cython.int, number_of_gas_products=cython.int) - cython.declare(number_of_surface_reactants=cython.int, number_of_surface_products=cython.int) - cython.declare(dN_surf=cython.int, dN_gas=cython.int, sites=cython.int) - cython.declare(sigma_nu=cython.double) - cython.declare(rectant=Species, product=Species, spcs=Species) + cython.declare( + dGrxn=cython.double, + K=cython.double, + C0=cython.double, + P0=cython.double, + dN_gas=cython.int, + dN_surf=cython.int, + sites=cython.int, + number_of_gas_reactants=cython.int, + number_of_gas_products=cython.int, + number_of_surface_reactants=cython.int, + number_of_surface_products=cython.int, + sigma_nu=cython.double, + rectant=Species, + product=Species, + spcs=Species, + ) # Use free energy of reaction to calculate Ka dGrxn = self.get_free_energy_of_reaction(T, potential) K = np.exp(-dGrxn / constants.R / T) From fecd202cece7b39d3e0f9c23e4443f3dc5e20e12 Mon Sep 17 00:00:00 2001 From: Jackson Burns Date: Thu, 1 Feb 2024 11:16:22 -0500 Subject: [PATCH 078/109] Ported all echem-related unit tests to new style --- rmgpy/kinetics/arrheniusTest.py | 1253 ----------------- test/database/databaseTest.py | 19 +- test/rmgpy/data/kinetics/familyTest.py | 47 + test/rmgpy/kinetics/arrheniusTest.py | 59 +- test/rmgpy/kinetics/kineticsSurfaceTest.py | 312 +++- test/rmgpy/molecule/atomtypeTest.py | 30 +- test/rmgpy/molecule/groupTest.py | 124 ++ test/rmgpy/molecule/moleculeTest.py | 32 +- test/rmgpy/reactionTest.py | 58 +- .../groups.py | 0 .../rules.py | 0 .../training/dictionary.txt | 0 .../training/reactions.py | 0 13 files changed, 621 insertions(+), 1313 deletions(-) delete mode 100644 rmgpy/kinetics/arrheniusTest.py rename {rmgpy => test/rmgpy}/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py (100%) rename {rmgpy => test/rmgpy}/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py (100%) rename {rmgpy => test/rmgpy}/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt (100%) rename {rmgpy => test/rmgpy}/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py (100%) diff --git a/rmgpy/kinetics/arrheniusTest.py b/rmgpy/kinetics/arrheniusTest.py deleted file mode 100644 index e3fdff52bf..0000000000 --- a/rmgpy/kinetics/arrheniusTest.py +++ /dev/null @@ -1,1253 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################### -# # -# RMG - Reaction Mechanism Generator # -# # -# Copyright (c) 2002-2021 Prof. William H. Green (whgreen@mit.edu), # -# Prof. Richard H. West (r.west@neu.edu) and the RMG Team (rmg_dev@mit.edu) # -# # -# 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 script contains unit tests of the :mod:`rmgpy.kinetics.arrhenius` module. -""" - -import math -import unittest - -import numpy as np - -import rmgpy.constants as constants -from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, ArrheniusBM, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius -from rmgpy.molecule.molecule import Molecule -from rmgpy.reaction import Reaction -from rmgpy.species import Species -from rmgpy.thermo import NASA, NASAPolynomial, ThermoData - - -################################################################################ - -class TestArrhenius(unittest.TestCase): - """ - Contains unit tests of the :class:`Arrhenius` class. - """ - - def setUp(self): - """ - A function run before each unit test in this class. - """ - self.A = 1.0e12 - self.n = 0.5 - self.Ea = 41.84 - self.T0 = 1. - self.Tmin = 300. - self.Tmax = 3000. - self.comment = 'C2H6' - self.arrhenius = Arrhenius( - A=(self.A, "cm^3/(mol*s)"), - n=self.n, - Ea=(self.Ea, "kJ/mol"), - T0=(self.T0, "K"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ) - - def test_a_factor(self): - """ - Test that the Arrhenius A property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.A.value_si * 1e6, self.A, delta=1e0) - - def test_n(self): - """ - Test that the Arrhenius n property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.n.value_si, self.n, 6) - - def test_ea(self): - """ - Test that the Arrhenius Ea property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.Ea.value_si * 0.001, self.Ea, 6) - - def test_temperature0(self): - """ - Test that the Arrhenius T0 property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.T0.value_si, self.T0, 6) - - def test_temperature_min(self): - """ - Test that the Arrhenius Tmin property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.Tmin.value_si, self.Tmin, 6) - - def test_temperature_max(self): - """ - Test that the Arrhenius Tmax property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.Tmax.value_si, self.Tmax, 6) - - def test_comment(self): - """ - Test that the Arrhenius comment property was properly set. - """ - self.assertEqual(self.arrhenius.comment, self.comment) - - def test_is_temperature_valid(self): - """ - Test the Arrhenius.is_temperature_valid() method. - """ - Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - validdata = np.array([False, True, True, True, True, True, True, True, True, True], np.bool) - for T, valid in zip(Tdata, validdata): - valid0 = self.arrhenius.is_temperature_valid(T) - self.assertEqual(valid0, valid) - - def test_get_rate_coefficient(self): - """ - Test the Arrhenius.get_rate_coefficient() method. - """ - Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - kexplist = np.array( - [1.6721e-4, 6.8770e1, 5.5803e3, 5.2448e4, 2.0632e5, 5.2285e5, 1.0281e6, 1.7225e6, 2.5912e6, 3.6123e6]) - for T, kexp in zip(Tlist, kexplist): - kact = self.arrhenius.get_rate_coefficient(T) - self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) - - def test_change_t0(self): - """ - Test the Arrhenius.change_t0() method. - """ - Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - k0list = np.array([self.arrhenius.get_rate_coefficient(T) for T in Tlist]) - self.arrhenius.change_t0(300) - self.assertEqual(self.arrhenius.T0.value_si, 300) - for T, kexp in zip(Tlist, k0list): - kact = self.arrhenius.get_rate_coefficient(T) - self.assertAlmostEqual(kexp, kact, delta=1e-6 * kexp) - - def test_fit_to_data(self): - """ - Test the Arrhenius.fit_to_data() method. - """ - Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - kdata = np.array([self.arrhenius.get_rate_coefficient(T) for T in Tdata]) - arrhenius = Arrhenius().fit_to_data(Tdata, kdata, kunits="m^3/(mol*s)") - self.assertEqual(float(self.arrhenius.T0.value_si), 1) - for T, k in zip(Tdata, kdata): - self.assertAlmostEqual(k, arrhenius.get_rate_coefficient(T), delta=1e-6 * k) - self.assertAlmostEqual(arrhenius.A.value_si, self.arrhenius.A.value_si, delta=1e0) - self.assertAlmostEqual(arrhenius.n.value_si, self.arrhenius.n.value_si, 1, 4) - self.assertAlmostEqual(arrhenius.Ea.value_si, self.arrhenius.Ea.value_si, 2) - self.assertAlmostEqual(arrhenius.T0.value_si, self.arrhenius.T0.value_si, 4) - - def test_fit_to_negative_data(self): - """ - Test the Arrhenius.fit_to_data() method on negative rates - """ - Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - kdata = np.array([-1 * self.arrhenius.get_rate_coefficient(T) for T in Tdata]) - arrhenius = Arrhenius().fit_to_data(Tdata, kdata, kunits="m^3/(mol*s)") - self.assertEqual(float(self.arrhenius.T0.value_si), 1) - for T, k in zip(Tdata, kdata): - self.assertAlmostEqual(k, arrhenius.get_rate_coefficient(T), delta=1e-6 * abs(k)) - self.assertAlmostEqual(arrhenius.A.value_si, -1 * self.arrhenius.A.value_si, delta=1e0) - self.assertAlmostEqual(arrhenius.n.value_si, self.arrhenius.n.value_si, 1, 4) - self.assertAlmostEqual(arrhenius.Ea.value_si, self.arrhenius.Ea.value_si, 2) - self.assertAlmostEqual(arrhenius.T0.value_si, self.arrhenius.T0.value_si, 4) - - def test_pickle(self): - """ - Test that an Arrhenius object can be pickled and unpickled with no loss - of information. - """ - import pickle - arrhenius = pickle.loads(pickle.dumps(self.arrhenius, -1)) - self.assertAlmostEqual(self.arrhenius.A.value, arrhenius.A.value, delta=1e0) - self.assertEqual(self.arrhenius.A.units, arrhenius.A.units) - self.assertAlmostEqual(self.arrhenius.n.value, arrhenius.n.value, 4) - self.assertAlmostEqual(self.arrhenius.Ea.value, arrhenius.Ea.value, 4) - self.assertEqual(self.arrhenius.Ea.units, arrhenius.Ea.units) - self.assertAlmostEqual(self.arrhenius.T0.value, arrhenius.T0.value, 4) - self.assertEqual(self.arrhenius.T0.units, arrhenius.T0.units) - self.assertAlmostEqual(self.arrhenius.Tmin.value, arrhenius.Tmin.value, 4) - self.assertEqual(self.arrhenius.Tmin.units, arrhenius.Tmin.units) - self.assertAlmostEqual(self.arrhenius.Tmax.value, arrhenius.Tmax.value, 4) - self.assertEqual(self.arrhenius.Tmax.units, arrhenius.Tmax.units) - self.assertEqual(self.arrhenius.comment, arrhenius.comment) - - def test_repr(self): - """ - Test that an Arrhenius object can be reconstructed from its repr() - output with no loss of information. - """ - namespace = {} - exec('arrhenius = {0!r}'.format(self.arrhenius), globals(), namespace) - self.assertIn('arrhenius', namespace) - arrhenius = namespace['arrhenius'] - self.assertAlmostEqual(self.arrhenius.A.value, arrhenius.A.value, delta=1e0) - self.assertEqual(self.arrhenius.A.units, arrhenius.A.units) - self.assertAlmostEqual(self.arrhenius.n.value, arrhenius.n.value, 4) - self.assertAlmostEqual(self.arrhenius.Ea.value, arrhenius.Ea.value, 4) - self.assertEqual(self.arrhenius.Ea.units, arrhenius.Ea.units) - self.assertAlmostEqual(self.arrhenius.T0.value, arrhenius.T0.value, 4) - self.assertEqual(self.arrhenius.T0.units, arrhenius.T0.units) - self.assertAlmostEqual(self.arrhenius.Tmin.value, arrhenius.Tmin.value, 4) - self.assertEqual(self.arrhenius.Tmin.units, arrhenius.Tmin.units) - self.assertAlmostEqual(self.arrhenius.Tmax.value, arrhenius.Tmax.value, 4) - self.assertEqual(self.arrhenius.Tmax.units, arrhenius.Tmax.units) - self.assertEqual(self.arrhenius.comment, arrhenius.comment) - - def test_change_rate(self): - """ - Test the Arrhenius.change_rate() method. - """ - Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - k0list = np.array([self.arrhenius.get_rate_coefficient(T) for T in Tlist]) - self.arrhenius.change_rate(2) - for T, kexp in zip(Tlist, k0list): - kact = self.arrhenius.get_rate_coefficient(T) - self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) - - def test_to_cantera_kinetics(self): - """ - Test that the Arrhenius cantera object can be set properly within - a cantera Reaction object - """ - ctArrhenius = self.arrhenius.to_cantera_kinetics() - self.assertAlmostEqual(ctArrhenius.pre_exponential_factor, 1e9, 6) - self.assertAlmostEqual(ctArrhenius.temperature_exponent, 0.5) - self.assertAlmostEqual(ctArrhenius.activation_energy, 41.84e6) - - def test_to_arrhenius_ep(self): - """ - Tests that the Arrhenius object can be converted to ArrheniusEP - """ - arr_rate = self.arrhenius.get_rate_coefficient(500) - arr_ep = self.arrhenius.to_arrhenius_ep() - arr_ep_rate = arr_ep.get_rate_coefficient(500, 10) # the second number should not matter - self.assertAlmostEqual(arr_rate, arr_ep_rate) - - def test_to_arrhenius_ep_with_alpha_and_hrxn(self): - """ - Tests that the Arrhenius object can be converted to ArrheniusEP given parameters - """ - hrxn = 5 - arr_rate = self.arrhenius.get_rate_coefficient(500) - arr_ep = self.arrhenius.to_arrhenius_ep(alpha=1, dHrxn=hrxn) - self.assertAlmostEqual(1., arr_ep.alpha.value_si) - arr_ep_rate = arr_ep.get_rate_coefficient(500, hrxn) - self.assertAlmostEqual(arr_rate, arr_ep_rate) - - def test_to_arrhenius_ep_throws_error_with_just_alpha(self): - with self.assertRaises(Exception): - self.arrhenius.to_arrhenius_ep(alpha=1) - - -################################################################################ - -class TestArrheniusEP(unittest.TestCase): - """ - Contains unit tests of the :class:`ArrheniusEP` class. - """ - - def setUp(self): - """ - A function run before each unit test in this class. - """ - self.A = 1.0e12 - self.n = 0.5 - self.alpha = 0.5 - self.E0 = 41.84 - self.Tmin = 300. - self.Tmax = 3000. - self.comment = 'C2H6' - self.arrhenius = ArrheniusEP( - A=(self.A, "cm^3/(mol*s)"), - n=self.n, - alpha=self.alpha, - E0=(self.E0, "kJ/mol"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ) - - def test_a_factor(self): - """ - Test that the ArrheniusEP A property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.A.value_si * 1e6, self.A, delta=1e0) - - def test_n(self): - """ - Test that the ArrheniusEP n property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.n.value_si, self.n, 6) - - def test_alpha(self): - """ - Test that the ArrheniusEP alpha property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.alpha.value_si, self.alpha, 6) - - def test_e0(self): - """ - Test that the ArrheniusEP E0 property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.E0.value_si * 0.001, self.E0, 6) - - def test_temperature_min(self): - """ - Test that the ArrheniusEP Tmin property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.Tmin.value_si, self.Tmin, 6) - - def test_temperature_max(self): - """ - Test that the ArrheniusEP Tmax property was properly set. - """ - self.assertAlmostEqual(self.arrhenius.Tmax.value_si, self.Tmax, 6) - - def test_comment(self): - """ - Test that the ArrheniusEP comment property was properly set. - """ - self.assertEqual(self.arrhenius.comment, self.comment) - - def test_is_temperature_valid(self): - """ - Test the ArrheniusEP.is_temperature_valid() method. - """ - Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - validdata = np.array([False, True, True, True, True, True, True, True, True, True], np.bool) - for T, valid in zip(Tdata, validdata): - valid0 = self.arrhenius.is_temperature_valid(T) - self.assertEqual(valid0, valid) - - def test_get_rate_coefficient(self): - """ - Test the ArrheniusEP.get_rate_coefficient() method. - """ - Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - kexplist = np.array( - [1.6721e-4, 6.8770e1, 5.5803e3, 5.2448e4, 2.0632e5, 5.2285e5, 1.0281e6, 1.7225e6, 2.5912e6, 3.6123e6]) - for T, kexp in zip(Tlist, kexplist): - kact = self.arrhenius.get_rate_coefficient(T, ) - self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) - - def test_pickle(self): - """ - Test that an ArrheniusEP object can be pickled and unpickled with no loss - of information. - """ - import pickle - arrhenius = pickle.loads(pickle.dumps(self.arrhenius, -1)) - self.assertAlmostEqual(self.arrhenius.A.value, arrhenius.A.value, delta=1e0) - self.assertEqual(self.arrhenius.A.units, arrhenius.A.units) - self.assertAlmostEqual(self.arrhenius.n.value, arrhenius.n.value, 4) - self.assertAlmostEqual(self.arrhenius.alpha.value, arrhenius.alpha.value, 4) - self.assertAlmostEqual(self.arrhenius.E0.value, arrhenius.E0.value, 4) - self.assertEqual(self.arrhenius.E0.units, arrhenius.E0.units) - self.assertAlmostEqual(self.arrhenius.Tmin.value, arrhenius.Tmin.value, 4) - self.assertEqual(self.arrhenius.Tmin.units, arrhenius.Tmin.units) - self.assertAlmostEqual(self.arrhenius.Tmax.value, arrhenius.Tmax.value, 4) - self.assertEqual(self.arrhenius.Tmax.units, arrhenius.Tmax.units) - self.assertEqual(self.arrhenius.comment, arrhenius.comment) - - def test_repr(self): - """ - Test that an ArrheniusEP object can be reconstructed from its repr() - output with no loss of information. - """ - namespace = {} - exec('arrhenius = {0!r}'.format(self.arrhenius), globals(), namespace) - self.assertIn('arrhenius', namespace) - arrhenius = namespace['arrhenius'] - self.assertAlmostEqual(self.arrhenius.A.value, arrhenius.A.value, delta=1e0) - self.assertEqual(self.arrhenius.A.units, arrhenius.A.units) - self.assertAlmostEqual(self.arrhenius.n.value, arrhenius.n.value, 4) - self.assertAlmostEqual(self.arrhenius.alpha.value, arrhenius.alpha.value, 4) - self.assertAlmostEqual(self.arrhenius.E0.value, arrhenius.E0.value, 4) - self.assertEqual(self.arrhenius.E0.units, arrhenius.E0.units) - self.assertAlmostEqual(self.arrhenius.Tmin.value, arrhenius.Tmin.value, 4) - self.assertEqual(self.arrhenius.Tmin.units, arrhenius.Tmin.units) - self.assertAlmostEqual(self.arrhenius.Tmax.value, arrhenius.Tmax.value, 4) - self.assertEqual(self.arrhenius.Tmax.units, arrhenius.Tmax.units) - self.assertEqual(self.arrhenius.comment, arrhenius.comment) - - def test_change_rate(self): - """ - Test the ArrheniusEP.change_rate() method. - """ - Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - k0list = np.array([self.arrhenius.get_rate_coefficient(T) for T in Tlist]) - self.arrhenius.change_rate(2) - for T, kexp in zip(Tlist, k0list): - kact = self.arrhenius.get_rate_coefficient(T) - self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) - - -################################################################################ - -class TestArrheniusBM(unittest.TestCase): - """ - Contains unit tests of the :class:`ArrheniusBM` class. - """ - - def setUp(self): - """ - A function run before each unit test in this class. - """ - self.A = 8.00037e+12 - self.n = 0.391734 - self.w0 = 798000 - self.E0 = 116249.32617478925 - self.Tmin = 300. - self.Tmax = 2000. - self.comment = 'rxn001084' - self.arrhenius_bm = ArrheniusBM( - A=(self.A, "s^-1"), - n=self.n, - w0=(self.w0, 'J/mol'), - E0=(self.E0, "J/mol"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ) - - self.rsmi = 'NC(=NC=O)O' - self.psmi = 'O=CNC(=O)N' - self.arrhenius = Arrhenius(A=(8.00037e+12,'s^-1'), - n=0.391734, - Ea=(94.5149,'kJ/mol'), - T0=(1,'K'), - Tmin=(300,'K'), - Tmax=(2000,'K'), - comment="""Fitted to 50 data points; dA = *|/ 1.18377, dn = +|- 0.0223855, dEa = +|- 0.115431 kJ/mol""" - ) - - self.r_thermo = NASA(polynomials=[ - NASAPolynomial(coeffs=[3.90453,0.0068491,0.000125755,-2.92973e-07,2.12971e-10,-45444.2,10.0669], Tmin=(10,'K'), Tmax=(433.425,'K')), - NASAPolynomial(coeffs=[2.09778,0.0367646,-2.36023e-05,7.24527e-09,-8.51275e-13,-45412,15.8381], Tmin=(433.425,'K'), Tmax=(3000,'K'))], - Tmin=(10,'K'), Tmax=(3000,'K'), E0=(-377.851,'kJ/mol'), Cp0=(33.2579,'J/(mol*K)'), CpInf=(232.805,'J/(mol*K)'), - comment="""Thermo library: Spiekermann_refining_elementary_reactions""" - ) - self.p_thermo = NASA(polynomials=[ - NASAPolynomial(coeffs=[3.88423,0.00825528,0.000133399,-3.31802e-07,2.52823e-10,-51045.1,10.3937], Tmin=(10,'K'), Tmax=(428.701,'K')), - NASAPolynomial(coeffs=[2.89294,0.0351772,-2.26349e-05,7.00331e-09,-8.2982e-13,-51122.5,12.4424], Tmin=(428.701,'K'), Tmax=(3000,'K'))], - Tmin=(10,'K'), Tmax=(3000,'K'), E0=(-424.419,'kJ/mol'), Cp0=(33.2579,'J/(mol*K)'), CpInf=(232.805,'J/(mol*K)'), - comment="""Thermo library: Spiekermann_refining_elementary_reactions""" - ) - - CF2 = Species().from_adjacency_list( - """ - 1 F u0 p3 c0 {2,S} - 2 C u0 p1 c0 {1,S} {3,S} - 3 F u0 p3 c0 {2,S} - """ - ) - CF2.thermo = NASA( - polynomials=[ - NASAPolynomial(coeffs=[2.28591,0.0107608,-1.05382e-05,4.89881e-09,-8.86384e-13,-24340.7,13.1348], Tmin=(298,'K'), Tmax=(1300,'K')), - NASAPolynomial(coeffs=[5.33121,0.00197748,-9.60248e-07,2.10704e-10,-1.5954e-14,-25190.9,-2.56367], Tmin=(1300,'K'), Tmax=(3000,'K')) - ], - Tmin=(298,'K'), Tmax=(3000,'K'), Cp0=(33.2579,'J/mol/K'), CpInf=(58.2013,'J/mol/K'), - comment="""Thermo library: halogens""" - ) - C2H6 = Species(smiles="CC") - C2H6.thermo = ThermoData( - Tdata = ([300,400,500,600,800,1000,1500],'K'), - Cpdata = ([12.565,15.512,18.421,21.059,25.487,28.964,34.591],'cal/(mol*K)','+|-',[0.8,1.1,1.3,1.4,1.5,1.5,1.2]), - H298 = (-20.028,'kcal/mol','+|-',0.1), - S298 = (54.726,'cal/(mol*K)','+|-',0.6), - comment="""Thermo library: DFT_QCI_thermo""" - ) - CH3CF2CH3 = Species(smiles="CC(F)(F)C") - CH3CF2CH3.thermo = NASA( - polynomials = [ - NASAPolynomial(coeffs=[3.89769,0.00706735,0.000140168,-3.37628e-07,2.51812e-10,-68682.1,8.74321], Tmin=(10,'K'), Tmax=(436.522,'K')), - NASAPolynomial(coeffs=[2.78849,0.0356982,-2.16715e-05,6.45057e-09,-7.47989e-13,-68761.2,11.1597], Tmin=(436.522,'K'), Tmax=(3000,'K')), - ], - Tmin = (10,'K'), Tmax = (3000,'K'), Cp0 = (33.2579,'J/(mol*K)'), CpInf = (249.434,'J/(mol*K)'), - comment="""Thermo library: CHOF_G4""" - ) - kinetics = Arrhenius(A=(0.222791,'cm^3/(mol*s)'), n=3.59921, Ea=(320.496,'kJ/mol'), T0=(1,'K'), Tmin=(298,'K'), Tmax=(2500,'K'), comment="""Training Rxn 54 for 1,2_Insertion_carbene""") - self.reaction = Reaction(reactants=[CF2,C2H6],products=[CH3CF2CH3],kinetics=kinetics) - self.reaction_w0 = 519000 # J/mol - - def test_a_factor(self): - """ - Test that the ArrheniusBM A property was properly set. - """ - self.assertAlmostEqual(self.arrhenius_bm.A.value_si, self.A, delta=1e0) - - def test_n(self): - """ - Test that the ArrheniusBM n property was properly set. - """ - self.assertAlmostEqual(self.arrhenius_bm.n.value_si, self.n, 6) - - def test_w0(self): - """ - Test that the ArrheniusBM w0 property was properly set. - """ - self.assertAlmostEqual(self.arrhenius_bm.w0.value_si, self.w0, 6) - - def test_e0(self): - """ - Test that the ArrheniusBM E0 property was properly set. - """ - self.assertAlmostEqual(self.arrhenius_bm.E0.value_si, self.E0, 6) - - def test_temperature_min(self): - """ - Test that the ArrheniusBM Tmin property was properly set. - """ - self.assertAlmostEqual(self.arrhenius_bm.Tmin.value_si, self.Tmin, 6) - - def test_temperature_max(self): - """ - Test that the ArrheniusBM Tmax property was properly set. - """ - self.assertAlmostEqual(self.arrhenius_bm.Tmax.value_si, self.Tmax, 6) - - def test_is_temperature_valid(self): - """ - Test the ArrheniusBM.is_temperature_valid() method. - """ - Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - validdata = np.array([False, True, True, True, True, True, True, True, True, True], np.bool) - for T, valid in zip(Tdata, validdata): - valid0 = self.arrhenius_bm.is_temperature_valid(T) - self.assertEqual(valid0, valid) - - def test_fit_to_data(self): - """ - Test the ArrheniusBM.fit_to_data() method. - """ - Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - - reactant = Molecule(smiles=self.rsmi) - product = Molecule(smiles=self.psmi) - reaction = Reaction(reactants=[Species(molecule=[reactant], thermo=self.r_thermo,)], - products=[Species(molecule=[product], thermo=self.p_thermo)], - kinetics=self.arrhenius, - ) - kdata = np.array([reaction.kinetics.get_rate_coefficient(T) for T in Tdata]) - arrhenius_bm = ArrheniusBM().fit_to_reactions([reaction], w0=self.w0) - arrhenius = arrhenius_bm.to_arrhenius(reaction.get_enthalpy_of_reaction(298)) - for T, k in zip(Tdata, kdata): - self.assertAlmostEqual(k, arrhenius.get_rate_coefficient(T), delta=1e-6 * k) - - # A second check, with a different reaction - arrhenius_bm = ArrheniusBM().fit_to_reactions([self.reaction], w0=self.reaction_w0) - arrhenius = arrhenius_bm.to_arrhenius(self.reaction.get_enthalpy_of_reaction(298)) - kdata = np.array([self.reaction.kinetics.get_rate_coefficient(T) for T in Tdata]) - for T, k in zip(Tdata, kdata): - self.assertAlmostEqual(k, arrhenius.get_rate_coefficient(T), delta=1e-6 * k) - - def test_get_activation_energy(self): - """ - Test the ArrheniusBM.get_activation_energy() method. - """ - Hrxn = -44000 # J/mol - Ea = self.arrhenius_bm.get_activation_energy(Hrxn) - - w = self.w0 - E0 = self.E0 - Vp = 2 * w * (w + E0)/(w - E0) - Ea_exp = (w + Hrxn/2) * (Vp - 2*w + Hrxn)**2 / (Vp*Vp - 4*w*w + Hrxn*Hrxn) - - self.assertAlmostEqual(Ea, Ea_exp, delta=1e1) - - -################################################################################ - -class TestPDepArrhenius(unittest.TestCase): - """ - Contains unit tests of the :class:`PDepArrhenius` class. - """ - - def setUp(self): - """ - A function run before each unit test in this class. - """ - self.arrhenius0 = Arrhenius( - A=(1.0e6, "s^-1"), - n=1.0, - Ea=(10.0, "kJ/mol"), - T0=(300.0, "K"), - Tmin=(300.0, "K"), - Tmax=(2000.0, "K"), - comment="""This data is completely made up""", - ) - self.arrhenius1 = Arrhenius( - A=(1.0e12, "s^-1"), - n=1.0, - Ea=(20.0, "kJ/mol"), - T0=(300.0, "K"), - Tmin=(300.0, "K"), - Tmax=(2000.0, "K"), - comment="""This data is completely made up""", - ) - self.pressures = np.array([0.1, 10.0]) - self.arrhenius = [self.arrhenius0, self.arrhenius1] - self.Tmin = 300.0 - self.Tmax = 2000.0 - self.Pmin = 0.1 - self.Pmax = 10.0 - self.comment = """This data is completely made up""" - self.kinetics = PDepArrhenius( - pressures=(self.pressures, "bar"), - arrhenius=self.arrhenius, - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - Pmin=(self.Pmin, "bar"), - Pmax=(self.Pmax, "bar"), - comment=self.comment, - ) - - def test_pressures(self): - """ - Test that the PDepArrhenius pressures property was properly set. - """ - self.assertEqual(len(self.kinetics.pressures.value_si), 2) - for i in range(2): - self.assertAlmostEqual(self.kinetics.pressures.value_si[i] * 1e-5, self.pressures[i], 4) - - def test_arrhenius(self): - """ - Test that the PDepArrhenius arrhenius property was properly set. - """ - self.assertEqual(len(self.kinetics.arrhenius), 2) - for i in range(2): - self.assertAlmostEqual(self.kinetics.arrhenius[i].A.value, self.arrhenius[i].A.value, delta=1e0) - self.assertEqual(self.kinetics.arrhenius[i].A.units, self.arrhenius[i].A.units) - self.assertAlmostEqual(self.kinetics.arrhenius[i].n.value, self.arrhenius[i].n.value, 4) - self.assertAlmostEqual(self.kinetics.arrhenius[i].Ea.value, self.arrhenius[i].Ea.value, 4) - self.assertEqual(self.kinetics.arrhenius[i].Ea.units, self.arrhenius[i].Ea.units) - self.assertAlmostEqual(self.kinetics.arrhenius[i].T0.value, self.arrhenius[i].T0.value, 4) - self.assertEqual(self.kinetics.arrhenius[i].T0.units, self.arrhenius[i].T0.units) - self.assertAlmostEqual(self.kinetics.arrhenius[i].Tmin.value, self.arrhenius[i].Tmin.value, 4) - self.assertEqual(self.kinetics.arrhenius[i].Tmin.units, self.arrhenius[i].Tmin.units) - self.assertAlmostEqual(self.kinetics.arrhenius[i].Tmax.value, self.arrhenius[i].Tmax.value, 4) - self.assertEqual(self.kinetics.arrhenius[i].Tmax.units, self.arrhenius[i].Tmax.units) - self.assertEqual(self.kinetics.arrhenius[i].comment, self.arrhenius[i].comment) - - def test_temperature_min(self): - """ - Test that the PDepArrhenius Tmin property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Tmin.value_si, self.Tmin, 6) - - def test_temperature_max(self): - """ - Test that the PDepArrhenius Tmax property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Tmax.value_si, self.Tmax, 6) - - def test_pressure_min(self): - """ - Test that the PDepArrhenius Pmin property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Pmin.value_si * 1e-5, self.Pmin, 6) - - def test_pressure_max(self): - """ - Test that the PDepArrhenius Pmax property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Pmax.value_si * 1e-5, self.Pmax, 6) - - def test_comment(self): - """ - Test that the PDepArrhenius comment property was properly set. - """ - self.assertEqual(self.kinetics.comment, self.comment) - - def test_is_pressure_dependent(self): - """ - Test the PDepArrhenius.is_pressure_dependent() method. - """ - self.assertTrue(self.kinetics.is_pressure_dependent()) - - def test_get_rate_coefficient(self): - """ - Test the PDepArrhenius.get_rate_coefficient() method. - """ - P = 1e4 - for T in [300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]: - k0 = self.kinetics.get_rate_coefficient(T, P) - k1 = self.arrhenius0.get_rate_coefficient(T) - self.assertAlmostEqual(k0, k1, delta=1e-6 * k1) - P = 1e6 - for T in [300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]: - k0 = self.kinetics.get_rate_coefficient(T, P) - k1 = self.arrhenius1.get_rate_coefficient(T) - self.assertAlmostEqual(k0, k1, delta=1e-6 * k1) - P = 1e5 - for T in [300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]: - k0 = self.kinetics.get_rate_coefficient(T, P) - k1 = math.sqrt(self.arrhenius0.get_rate_coefficient(T) * self.arrhenius1.get_rate_coefficient(T)) - self.assertAlmostEqual(k0, k1, delta=1e-6 * k1) - - def test_fit_to_data(self): - """ - Test the PDepArrhenius.fit_to_data() method. - """ - Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500], np.float) - Pdata = np.array([1e4, 3e4, 1e5, 3e5, 1e6], np.float) - kdata = np.zeros([len(Tdata), len(Pdata)], np.float) - for t in range(len(Tdata)): - for p in range(len(Pdata)): - kdata[t, p] = self.kinetics.get_rate_coefficient(Tdata[t], Pdata[p]) - kinetics = PDepArrhenius().fit_to_data(Tdata, Pdata, kdata, kunits="s^-1") - for t in range(len(Tdata)): - for p in range(len(Pdata)): - self.assertAlmostEqual(kinetics.get_rate_coefficient(Tdata[t], Pdata[p]), kdata[t, p], - delta=1e-6 * kdata[t, p]) - - def test_pickle(self): - """ - Test that a PDepArrhenius object can be successfully pickled and - unpickled with no loss of information. - """ - import pickle - kinetics = pickle.loads(pickle.dumps(self.kinetics, -1)) - Narrh = 2 - self.assertEqual(len(self.kinetics.pressures.value), Narrh) - self.assertEqual(len(kinetics.pressures.value), Narrh) - self.assertEqual(len(self.kinetics.arrhenius), Narrh) - self.assertEqual(len(kinetics.arrhenius), Narrh) - for i in range(Narrh): - self.assertAlmostEqual(self.kinetics.pressures.value[i], kinetics.pressures.value[i], 4) - self.assertAlmostEqual(self.kinetics.arrhenius[i].A.value, kinetics.arrhenius[i].A.value, delta=1e0) - self.assertEqual(self.kinetics.arrhenius[i].A.units, kinetics.arrhenius[i].A.units) - self.assertAlmostEqual(self.kinetics.arrhenius[i].n.value, kinetics.arrhenius[i].n.value) - self.assertAlmostEqual(self.kinetics.arrhenius[i].T0.value, kinetics.arrhenius[i].T0.value, 4) - self.assertEqual(self.kinetics.arrhenius[i].T0.units, kinetics.arrhenius[i].T0.units) - self.assertAlmostEqual(self.kinetics.arrhenius[i].Ea.value, kinetics.arrhenius[i].Ea.value, 4) - self.assertEqual(self.kinetics.arrhenius[i].Ea.units, kinetics.arrhenius[i].Ea.units) - self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) - self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) - self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) - self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) - self.assertAlmostEqual(self.kinetics.Pmin.value, kinetics.Pmin.value, 4) - self.assertEqual(self.kinetics.Pmin.units, kinetics.Pmin.units) - self.assertAlmostEqual(self.kinetics.Pmax.value, kinetics.Pmax.value, 4) - self.assertEqual(self.kinetics.Pmax.units, kinetics.Pmax.units) - self.assertEqual(self.kinetics.comment, kinetics.comment) - - def test_repr(self): - """ - Test that a PDepArrhenius object can be successfully reconstructed - from its repr() output with no loss of information. - """ - namespace = {} - exec('kinetics = {0!r}'.format(self.kinetics), globals(), namespace) - self.assertIn('kinetics', namespace) - kinetics = namespace['kinetics'] - Narrh = 2 - self.assertEqual(len(self.kinetics.pressures.value), Narrh) - self.assertEqual(len(kinetics.pressures.value), Narrh) - self.assertEqual(len(self.kinetics.arrhenius), Narrh) - self.assertEqual(len(kinetics.arrhenius), Narrh) - for i in range(Narrh): - self.assertAlmostEqual(self.kinetics.pressures.value[i], kinetics.pressures.value[i], 4) - self.assertAlmostEqual(self.kinetics.arrhenius[i].A.value, kinetics.arrhenius[i].A.value, delta=1e0) - self.assertEqual(self.kinetics.arrhenius[i].A.units, kinetics.arrhenius[i].A.units) - self.assertAlmostEqual(self.kinetics.arrhenius[i].n.value, kinetics.arrhenius[i].n.value) - self.assertAlmostEqual(self.kinetics.arrhenius[i].T0.value, kinetics.arrhenius[i].T0.value, 4) - self.assertEqual(self.kinetics.arrhenius[i].T0.units, kinetics.arrhenius[i].T0.units) - self.assertAlmostEqual(self.kinetics.arrhenius[i].Ea.value, kinetics.arrhenius[i].Ea.value, 4) - self.assertEqual(self.kinetics.arrhenius[i].Ea.units, kinetics.arrhenius[i].Ea.units) - self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) - self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) - self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) - self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) - self.assertAlmostEqual(self.kinetics.Pmin.value, kinetics.Pmin.value, 4) - self.assertEqual(self.kinetics.Pmin.units, kinetics.Pmin.units) - self.assertAlmostEqual(self.kinetics.Pmax.value, kinetics.Pmax.value, 4) - self.assertEqual(self.kinetics.Pmax.units, kinetics.Pmax.units) - self.assertEqual(self.kinetics.comment, kinetics.comment) - - def test_change_rate(self): - """ - Test the PDepArrhenius.change_rate() method. - """ - Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - k0list = np.array([self.kinetics.get_rate_coefficient(T, 1e5) for T in Tlist]) - self.kinetics.change_rate(2) - for T, kexp in zip(Tlist, k0list): - kact = self.kinetics.get_rate_coefficient(T, 1e5) - self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) - - -################################################################################ - -class TestMultiArrhenius(unittest.TestCase): - """ - Contains unit tests of the :class:`MultiArrhenius` class. - """ - - def setUp(self): - """ - A function run before each unit test in this class. - """ - self.Tmin = 350. - self.Tmax = 1500. - self.comment = 'Comment' - self.arrhenius = [ - Arrhenius( - A=(9.3e-14, "cm^3/(molecule*s)"), - n=0.0, - Ea=(4740 * constants.R * 0.001, "kJ/mol"), - T0=(1, "K"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ), - Arrhenius( - A=(1.4e-9, "cm^3/(molecule*s)"), - n=0.0, - Ea=(11200 * constants.R * 0.001, "kJ/mol"), - T0=(1, "K"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ), - ] - self.kinetics = MultiArrhenius( - arrhenius=self.arrhenius, - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ) - self.single_kinetics = MultiArrhenius( - arrhenius=self.arrhenius[:1], - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ) - - def test_arrhenius(self): - """ - Test that the MultiArrhenius A property was properly set. - """ - self.assertEqual(self.kinetics.arrhenius, self.arrhenius) - - def test_temperature_min(self): - """ - Test that the MultiArrhenius Tmin property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Tmin.value_si, self.Tmin, 6) - - def test_temperature_max(self): - """ - Test that the MultiArrhenius Tmax property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Tmax.value_si, self.Tmax, 6) - - def test_comment(self): - """ - Test that the MultiArrhenius comment property was properly set. - """ - self.assertEqual(self.kinetics.comment, self.comment) - - def test_is_temperature_valid(self): - """ - Test the MultiArrhenius.is_temperature_valid() method. - """ - Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - validdata = np.array([False, True, True, True, True, True, True, False, False, False], np.bool) - for T, valid in zip(Tdata, validdata): - valid0 = self.kinetics.is_temperature_valid(T) - self.assertEqual(valid0, valid) - - def test_get_rate_coefficient(self): - """ - Test the MultiArrhenius.get_rate_coefficient() method. - """ - Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - kexplist = np.array( - [2.85400e-06, 4.00384e-01, 2.73563e+01, 8.50699e+02, 1.20181e+04, 7.56312e+04, 2.84724e+05, 7.71702e+05, - 1.67743e+06, 3.12290e+06]) - for T, kexp in zip(Tlist, kexplist): - kact = self.kinetics.get_rate_coefficient(T) - self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) - - def test_pickle(self): - """ - Test that a MultiArrhenius object can be pickled and unpickled with no loss - of information. - """ - import pickle - kinetics = pickle.loads(pickle.dumps(self.kinetics, -1)) - self.assertEqual(len(self.kinetics.arrhenius), len(kinetics.arrhenius)) - for arrh0, arrh in zip(self.kinetics.arrhenius, kinetics.arrhenius): - self.assertAlmostEqual(arrh0.A.value, arrh.A.value, delta=1e-18) - self.assertEqual(arrh0.A.units, arrh.A.units) - self.assertAlmostEqual(arrh0.n.value, arrh.n.value, 4) - self.assertAlmostEqual(arrh0.Ea.value, arrh.Ea.value, 4) - self.assertEqual(arrh0.Ea.units, arrh.Ea.units) - self.assertAlmostEqual(arrh0.T0.value, arrh.T0.value, 4) - self.assertEqual(arrh0.T0.units, arrh.T0.units) - self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) - self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) - self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) - self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) - self.assertEqual(self.kinetics.comment, kinetics.comment) - - def test_repr(self): - """ - Test that a MultiArrhenius object can be reconstructed from its repr() - output with no loss of information. - """ - namespace = {} - exec('kinetics = {0!r}'.format(self.kinetics), globals(), namespace) - self.assertIn('kinetics', namespace) - kinetics = namespace['kinetics'] - self.assertEqual(len(self.kinetics.arrhenius), len(kinetics.arrhenius)) - for arrh0, arrh in zip(self.kinetics.arrhenius, kinetics.arrhenius): - self.assertAlmostEqual(arrh0.A.value, arrh.A.value, delta=1e-18) - self.assertEqual(arrh0.A.units, arrh.A.units) - self.assertAlmostEqual(arrh0.n.value, arrh.n.value, 4) - self.assertAlmostEqual(arrh0.Ea.value, arrh.Ea.value, 4) - self.assertEqual(arrh0.Ea.units, arrh.Ea.units) - self.assertAlmostEqual(arrh0.T0.value, arrh.T0.value, 4) - self.assertEqual(arrh0.T0.units, arrh.T0.units) - self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) - self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) - self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) - self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) - self.assertEqual(self.kinetics.comment, kinetics.comment) - - def test_to_arrhenius(self): - """ - Test that we can convert to an Arrhenius - """ - answer = self.single_kinetics.arrhenius[0] - fitted = self.single_kinetics.to_arrhenius() - - self.assertAlmostEqual(fitted.A.value_si, answer.A.value_si, delta=1e0) - self.assertAlmostEqual(fitted.n.value_si, answer.n.value_si, 1, 4) - self.assertAlmostEqual(fitted.Ea.value_si, answer.Ea.value_si, 2) - self.assertAlmostEqual(fitted.T0.value_si, answer.T0.value_si, 4) - - def test_to_arrhenius_temperature_range(self): - """ - Test the to_arrhenius temperature range is set correctly. - """ - answer = self.single_kinetics.arrhenius[0] - fitted = self.single_kinetics.to_arrhenius(Tmin=800, Tmax=1200) - self.assertAlmostEqual(fitted.Tmin.value_si, 800.0) - self.assertAlmostEqual(fitted.Tmax.value_si, 1200.0) - for T in [800, 1000, 1200]: - self.assertAlmostEqual(fitted.get_rate_coefficient(T) / answer.get_rate_coefficient(T), 1.0) - - def test_to_arrhenius_multiple(self): - """ - Test the to_arrhenius fitting multiple kinetics over a small range, see if we're within 5% at a few points - """ - answer = self.kinetics - fitted = self.kinetics.to_arrhenius(Tmin=800, Tmax=1200) - self.assertAlmostEqual(fitted.Tmin.value_si, 800.0) - self.assertAlmostEqual(fitted.Tmax.value_si, 1200.0) - for T in [800, 1000, 1200]: - self.assertAlmostEqual(fitted.get_rate_coefficient(T) / answer.get_rate_coefficient(T), 1.0, delta=0.05) - - def test_change_rate(self): - """ - Test the MultiArrhenius.change_rate() method. - """ - Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - k0list = np.array([self.kinetics.get_rate_coefficient(T) for T in Tlist]) - self.kinetics.change_rate(2) - for T, kexp in zip(Tlist, k0list): - kact = self.kinetics.get_rate_coefficient(T) - self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) - - -################################################################################ - -class TestMultiPDepArrhenius(unittest.TestCase): - """ - Contains unit tests of the :class:`MultiPDepArrhenius` class. - """ - - def setUp(self): - """ - A function run before each unit test in this class. - """ - self.Tmin = 350. - self.Tmax = 1500. - self.Pmin = 1e-1 - self.Pmax = 1e1 - self.pressures = np.array([1e-1, 1e1]) - self.comment = 'CH3 + C2H6 <=> CH4 + C2H5 (Baulch 2005)' - self.arrhenius = [ - PDepArrhenius( - pressures=(self.pressures, "bar"), - arrhenius=[ - Arrhenius( - A=(9.3e-16, "cm^3/(molecule*s)"), - n=0.0, - Ea=(4740 * constants.R * 0.001, "kJ/mol"), - T0=(1, "K"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ), - Arrhenius( - A=(9.3e-14, "cm^3/(molecule*s)"), - n=0.0, - Ea=(4740 * constants.R * 0.001, "kJ/mol"), - T0=(1, "K"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ), - ], - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - Pmin=(self.Pmin, "bar"), - Pmax=(self.Pmax, "bar"), - comment=self.comment, - ), - PDepArrhenius( - pressures=(self.pressures, "bar"), - arrhenius=[ - Arrhenius( - A=(1.4e-11, "cm^3/(molecule*s)"), - n=0.0, - Ea=(11200 * constants.R * 0.001, "kJ/mol"), - T0=(1, "K"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ), - Arrhenius( - A=(1.4e-9, "cm^3/(molecule*s)"), - n=0.0, - Ea=(11200 * constants.R * 0.001, "kJ/mol"), - T0=(1, "K"), - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - comment=self.comment, - ), - ], - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - Pmin=(self.Pmin, "bar"), - Pmax=(self.Pmax, "bar"), - comment=self.comment, - ), - ] - self.kinetics = MultiPDepArrhenius( - arrhenius=self.arrhenius, - Tmin=(self.Tmin, "K"), - Tmax=(self.Tmax, "K"), - Pmin=(self.Pmin, "bar"), - Pmax=(self.Pmax, "bar"), - comment=self.comment, - ) - - def test_arrhenius(self): - """ - Test that the MultiPDepArrhenius arrhenius property was properly set. - """ - self.assertEqual(self.kinetics.arrhenius, self.arrhenius) - - def test_temperature_min(self): - """ - Test that the MultiPDepArrhenius Tmin property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Tmin.value_si, self.Tmin, 6) - - def test_temperature_max(self): - """ - Test that the MultiPDepArrhenius Tmax property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Tmax.value_si, self.Tmax, 6) - - def test_pressure_min(self): - """ - Test that the MultiPDepArrhenius Pmin property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Pmin.value_si * 1e-5, self.Pmin, 6) - - def test_pressure_max(self): - """ - Test that the MultiPDepArrhenius Pmax property was properly set. - """ - self.assertAlmostEqual(self.kinetics.Pmax.value_si * 1e-5, self.Pmax, 6) - - def test_comment(self): - """ - Test that the MultiPDepArrhenius comment property was properly set. - """ - self.assertEqual(self.kinetics.comment, self.comment) - - def test_is_temperature_valid(self): - """ - Test the MultiPDepArrhenius.is_temperature_valid() method. - """ - Tdata = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - validdata = np.array([False, True, True, True, True, True, True, False, False, False], np.bool) - for T, valid in zip(Tdata, validdata): - valid0 = self.kinetics.is_temperature_valid(T) - self.assertEqual(valid0, valid) - - def test_is_pressure_valid(self): - """ - Test the MultiPDepArrhenius.is_pressure_valid() method. - """ - Pdata = np.array([1e3, 1e4, 1e5, 1e6, 1e7]) - validdata = np.array([False, True, True, True, False], np.bool) - for P, valid in zip(Pdata, validdata): - valid0 = self.kinetics.is_pressure_valid(P) - self.assertEqual(valid0, valid) - - def test_get_rate_coefficient(self): - """ - Test the MultiPDepArrhenius.get_rate_coefficient() method. - """ - Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - Plist = np.array([1e4, 1e5, 1e6]) - kexplist = np.array([ - [2.85400e-08, 4.00384e-03, 2.73563e-01, 8.50699e+00, 1.20181e+02, 7.56312e+02, 2.84724e+03, 7.71702e+03, - 1.67743e+04, 3.12290e+04], - [2.85400e-07, 4.00384e-02, 2.73563e+00, 8.50699e+01, 1.20181e+03, 7.56312e+03, 2.84724e+04, 7.71702e+04, - 1.67743e+05, 3.12290e+05], - [2.85400e-06, 4.00384e-01, 2.73563e+01, 8.50699e+02, 1.20181e+04, 7.56312e+04, 2.84724e+05, 7.71702e+05, - 1.67743e+06, 3.12290e+06], - ]).T - for i in range(Tlist.shape[0]): - for j in range(Plist.shape[0]): - kexp = kexplist[i, j] - kact = self.kinetics.get_rate_coefficient(Tlist[i], Plist[j]) - self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) - - def test_get_rate_coefficient_diff_plist(self): - """ - Test the MultiPDepArrhenius.get_rate_coefficient() when plists are different. - """ - # modify the MultiPDepArrhenius object with an additional entry - pressures = np.array([1e-1, 1e-1, 1e1]) - self.kinetics.arrhenius[0].pressures = (pressures, "bar") - self.kinetics.arrhenius[0].arrhenius.insert(0, self.kinetics.arrhenius[0].arrhenius[0]) - - Tlist = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]) - Plist = np.array([1e4, 1e5, 1e6]) - kexplist = np.array([ - [2.85400e-08, 4.00384e-03, 2.73563e-01, 8.50699e+00, 1.20181e+02, 7.56312e+02, 2.84724e+03, 7.71702e+03, - 1.67743e+04, 3.12290e+04], - [2.85400e-07, 4.00384e-02, 2.73563e+00, 8.50699e+01, 1.20181e+03, 7.56312e+03, 2.84724e+04, 7.71702e+04, - 1.67743e+05, 3.12290e+05], - [2.85400e-06, 4.00384e-01, 2.73563e+01, 8.50699e+02, 1.20181e+04, 7.56312e+04, 2.84724e+05, 7.71702e+05, - 1.67743e+06, 3.12290e+06], - ]).T - for i in range(Tlist.shape[0]): - for j in range(Plist.shape[0]): - kexp = kexplist[i, j] - kact = self.kinetics.get_rate_coefficient(Tlist[i], Plist[j]) - self.assertAlmostEqual(kexp, kact, delta=1e-4 * kexp) - - def test_pickle(self): - """ - Test that a MultiPDepArrhenius object can be pickled and unpickled with - no loss of information. - """ - import pickle - kinetics = pickle.loads(pickle.dumps(self.kinetics, -1)) - self.assertEqual(len(self.kinetics.arrhenius), len(kinetics.arrhenius)) - self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) - self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) - self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) - self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) - self.assertEqual(self.kinetics.comment, kinetics.comment) - - def test_repr(self): - """ - Test that a MultiPDepArrhenius object can be reconstructed from its - repr() output with no loss of information. - """ - namespace = {} - exec('kinetics = {0!r}'.format(self.kinetics), globals(), namespace) - self.assertIn('kinetics', namespace) - kinetics = namespace['kinetics'] - self.assertEqual(len(self.kinetics.arrhenius), len(kinetics.arrhenius)) - self.assertAlmostEqual(self.kinetics.Tmin.value, kinetics.Tmin.value, 4) - self.assertEqual(self.kinetics.Tmin.units, kinetics.Tmin.units) - self.assertAlmostEqual(self.kinetics.Tmax.value, kinetics.Tmax.value, 4) - self.assertEqual(self.kinetics.Tmax.units, kinetics.Tmax.units) - self.assertEqual(self.kinetics.comment, kinetics.comment) - - def test_change_rate(self): - """ - Test the PDepMultiArrhenius.change_rate() method. - """ - Tlist = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) - k0list = np.array([self.kinetics.get_rate_coefficient(T, 1e5) for T in Tlist]) - self.kinetics.change_rate(2) - for T, kexp in zip(Tlist, k0list): - kact = self.kinetics.get_rate_coefficient(T, 1e5) - self.assertAlmostEqual(2 * kexp, kact, delta=1e-6 * kexp) - - def test_generate_reverse_rate_coefficient(self): - """ - Test ability to reverse a reaction rate. - - This is a real example from an imported chemkin file. - """ - from rmgpy.species import Species - from rmgpy.molecule import Molecule - from rmgpy.data.kinetics import LibraryReaction - from rmgpy.thermo import NASA, NASAPolynomial - test_reaction = LibraryReaction(reactants=[Species(label="C2H3", thermo=NASA(polynomials=[NASAPolynomial(coeffs=[3.12502,0.00235137,2.36803e-05,-3.35092e-08,1.39444e-11,34524.3,8.81538], Tmin=(200,"K"), Tmax=(1000,"K")), NASAPolynomial(coeffs=[4.37211,0.00746869,-2.64716e-06,4.22753e-10,-2.44958e-14,33805.2,0.428772], Tmin=(1000,"K"), Tmax=(6000,"K"))], Tmin=(200,"K"), Tmax=(6000,"K"), E0=(285.696,"kJ/mol"), Cp0=(33.2579,"J/mol/K"), CpInf=(108.088,"J/mol/K"), comment="""ATcT3E\nC2H3 ATcT ver. 1.122, DHf298 = 296.91 ± 0.33 kJ/mol - fit JAN17"""), molecule=[Molecule(smiles="[CH]=C")], molecular_weight=(27.0452,"amu")), - Species(label="CH2O", thermo=NASA(polynomials=[NASAPolynomial(coeffs=[4.77187,-0.00976266,3.70122e-05,-3.76922e-08,1.31327e-11,-14379.8,0.696586], Tmin=(200,"K"), Tmax=(1000,"K")), NASAPolynomial(coeffs=[2.91333,0.0067004,-2.55521e-06,4.27795e-10,-2.44073e-14,-14462.2,7.43823], Tmin=(1000,"K"), Tmax=(6000,"K"))], Tmin=(200,"K"), Tmax=(6000,"K"), E0=(-119.527,"kJ/mol"), Cp0=(33.2579,"J/mol/K"), CpInf=(83.1447,"J/mol/K"), comment="""ATcT3E\nH2CO ATcT ver. 1.122, DHf298 = -109.188 ± 0.099 kJ/mol - fit JAN17"""), molecule=[Molecule(smiles="C=O")], molecular_weight=(30.026,"amu"))], - products=[Species(label="C2H4", thermo=NASA(polynomials=[NASAPolynomial(coeffs=[3.65151,-0.00535067,5.16486e-05,-6.36869e-08,2.50743e-11,5114.51,5.38561], Tmin=(200,"K"), Tmax=(1000,"K")), NASAPolynomial(coeffs=[4.14446,0.0102648,-3.61247e-06,5.74009e-10,-3.39296e-14,4190.59,-1.14778], Tmin=(1000,"K"), Tmax=(6000,"K"))], Tmin=(200,"K"), Tmax=(6000,"K"), E0=(42.06,"kJ/mol"), Cp0=(33.2579,"J/mol/K"), CpInf=(133.032,"J/mol/K"), comment="""ATcT3E\nC2H4 ATcT ver. 1.122, DHf298 = 52.45 ± 0.13 kJ/mol - fit JAN17"""), molecule=[Molecule(smiles="C=C")], molecular_weight=(28.0532,"amu")), - Species(label="HCO", thermo=NASA(polynomials=[NASAPolynomial(coeffs=[3.97075,-0.00149122,9.54042e-06,-8.8272e-09,2.67645e-12,3842.03,4.4466], Tmin=(200,"K"), Tmax=(1000,"K")), NASAPolynomial(coeffs=[3.85781,0.00264114,-7.44177e-07,1.23313e-10,-8.88959e-15,3616.43,3.92451], Tmin=(1000,"K"), Tmax=(6000,"K"))], Tmin=(200,"K"), Tmax=(6000,"K"), E0=(32.0237,"kJ/mol"), Cp0=(33.2579,"J/mol/K"), CpInf=(58.2013,"J/mol/K"), comment="""HCO ATcT ver. 1.122, DHf298 = 41.803 ± 0.099 kJ/mol - fit JAN17"""), molecule=[Molecule(smiles="[CH]=O")], molecular_weight=(29.018,"amu"))], - kinetics=MultiPDepArrhenius(arrhenius=[PDepArrhenius(pressures=([0.001,0.01,0.1,1,10,100,1000],"atm"), - arrhenius=[Arrhenius(A=(1.1e+07,"cm^3/(mol*s)"), n=1.09, Ea=(1807,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(2.5e+07,"cm^3/(mol*s)"), n=0.993, Ea=(1995,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(2.5e+08,"cm^3/(mol*s)"), n=0.704, Ea=(2596,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(1.4e+10,"cm^3/(mol*s)"), n=0.209, Ea=(3934,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(3.5e+13,"cm^3/(mol*s)"), n=-0.726, Ea=(6944,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(3.3e+14,"cm^3/(mol*s)"), n=-0.866, Ea=(10966,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(17,"cm^3/(mol*s)"), n=3.17, Ea=(9400,"cal/mol"), T0=(1,"K"))]), - PDepArrhenius(pressures=([0.001,0.01,0.1,1,10,100,1000],"atm"), - arrhenius=[Arrhenius(A=(-2.3e+16,"cm^3/(mol*s)"), n=-1.269, Ea=(20617,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(-5.2e+16,"cm^3/(mol*s)"), n=-1.366, Ea=(20805,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(-1.5e+18,"cm^3/(mol*s)"), n=-1.769, Ea=(22524,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(-8.5e+19,"cm^3/(mol*s)"), n=-2.264, Ea=(23862,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(-4.4e+23,"cm^3/(mol*s)"), n=-3.278, Ea=(27795,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(-4.2e+24,"cm^3/(mol*s)"), n=-3.418, Ea=(31817,"cal/mol"), T0=(1,"K")), - Arrhenius(A=(-2.1e+11,"cm^3/(mol*s)"), n=0.618, Ea=(30251,"cal/mol"), T0=(1,"K"))]) - ]), duplicate=True) - test_reaction.generate_reverse_rate_coefficient() - -################################################################################ - -if __name__ == '__main__': - unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) diff --git a/test/database/databaseTest.py b/test/database/databaseTest.py index 4a6c73a45c..d4285d9f97 100644 --- a/test/database/databaseTest.py +++ b/test/database/databaseTest.py @@ -130,11 +130,15 @@ def test_kinetics(self): assert self.kinetics_check_training_reactions_have_surface_attributes( family_name ), "Kinetics surface family {0}: entries have surface attributes?".format(family_name) - - with check: - assert self.kinetics_check_coverage_dependence_units_are_correct( - family_name - ), "Kinetics surface family {0}: check coverage dependent units are correct?".format(family_name) + if family_name not in { + "Surface_Proton_Electron_Reduction_Alpha", + "Surface_Proton_Electron_Reduction_Beta", + "Surface_Proton_Electron_Reduction_Beta_Dissociation", + }: + with check: + assert self.kinetics_check_coverage_dependence_units_are_correct( + family_name + ), "Kinetics surface family {0}: check coverage dependent units are correct?".format(family_name) # these families have some sort of difficulty which prevents us from testing accessibility right now # See RMG-Py PR #2232 for reason why adding Bimolec_Hydroperoxide_Decomposition here. Basically some nodes @@ -974,7 +978,6 @@ def kinetics_check_library_rates_are_reasonable(self, library): if entry.item.is_surface_reaction() or isinstance(entry.data, KineticsModel): # Don't check surface reactions continue - k = entry.data.get_rate_coefficient(T, P) rxn = entry.item if k < 0: @@ -1521,7 +1524,7 @@ def kinetics_check_sample_can_react(self, family_name): backbone_msg += backbone_sample.item.to_adjacency_list() else: backbone_msg = "" - tst3.append( + test1.append( ( False, """ @@ -1978,7 +1981,7 @@ def general_check_sample_descends_to_group(self, group_name, group): tst3 = [] # Solvation groups have special groups that RMG cannot generate proper sample_molecules. Skip them. - skip_entry_list = ["Cds-CdsCS6dd", "Cs-CS4dHH"] + skip_entry_list = ["Cds-CdsCS6dd", "Cs-CS4dHH", 'Li-OCring', 'CsOOOring', 'Cbf-CbfCbfCbf'] skip_short_desc_list = [ "special solvation group with ring", "special solvation polycyclic group", diff --git a/test/rmgpy/data/kinetics/familyTest.py b/test/rmgpy/data/kinetics/familyTest.py index f28a06ae1b..2ce15fc206 100644 --- a/test/rmgpy/data/kinetics/familyTest.py +++ b/test/rmgpy/data/kinetics/familyTest.py @@ -44,6 +44,7 @@ from rmgpy.data.thermo import ThermoDatabase from rmgpy.molecule import Molecule from rmgpy.species import Species +from rmgpy.reaction import Reaction from rmgpy.kinetics import Arrhenius import pytest @@ -70,6 +71,7 @@ def setup_class(cls): "intra_substitutionS_isomerization", "R_Addition_COm", "R_Recombination", + 'Surface_Proton_Electron_Reduction_Alpha', ], ) cls.family = cls.database.families["intra_H_migration"] @@ -701,6 +703,51 @@ def test_save_family(self): that the objects are the same in memory. """ pass + + def test_surface_proton_electron_reduction_alpha(self): + """ + Test that the Surface_Proton_Electron_Reduction_Alpha family can successfully match the reaction and returns properly product structures. + """ + family = self.database.families['Surface_Proton_Electron_Reduction_Alpha'] + m_proton = Molecule().from_smiles("[H+]") + m_x = Molecule().from_adjacency_list("1 X u0 p0") + m_ch2x = Molecule().from_adjacency_list( + """ + 1 C u0 p0 c0 {2,S} {3,S} {4,D} + 2 H u0 p0 c0 {1,S} + 3 H u0 p0 c0 {1,S} + 4 X u0 p0 c0 {1,D} + """ + ) + m_ch3x = Molecule().from_adjacency_list( + """ + 1 C u0 p0 c0 {2,S} {3,S} {4,S} {5,S} + 2 H u0 p0 c0 {1,S} + 3 H u0 p0 c0 {1,S} + 4 H u0 p0 c0 {1,S} + 5 X u0 p0 c0 {1,S} + """ + ) + + reactants = [m_proton,m_ch2x] + expected_products = [m_ch3x] + + labeled_rxn = Reaction(reactants=reactants, products=expected_products) + family.add_atom_labels_for_reaction(labeled_rxn) + prods = family.apply_recipe([m.molecule[0] for m in labeled_rxn.reactants]) + assert expected_products[0].is_isomorphic(prods[0]) + + assert len(prods) == 1 + assert expected_products[0].is_isomorphic(prods[0]) + reacts = family.apply_recipe(prods, forward=False) + assert len(reacts) == 2 + + prods = [Species(molecule=[p]) for p in prods] + reacts = [Species(molecule=[r]) for r in reacts] + + fam_rxn = Reaction(reactants=reacts,products=prods) + + assert fam_rxn.is_isomorphic(labeled_rxn) def test_reactant_num_id(self): """ diff --git a/test/rmgpy/kinetics/arrheniusTest.py b/test/rmgpy/kinetics/arrheniusTest.py index ee3f271829..5913c851e9 100644 --- a/test/rmgpy/kinetics/arrheniusTest.py +++ b/test/rmgpy/kinetics/arrheniusTest.py @@ -48,7 +48,7 @@ from rmgpy.molecule.molecule import Molecule from rmgpy.reaction import Reaction from rmgpy.species import Species -from rmgpy.thermo import NASA, NASAPolynomial +from rmgpy.thermo import NASA, NASAPolynomial, ThermoData import pytest @@ -443,7 +443,7 @@ def setup_method(self): self.A = 8.00037e12 self.n = 0.391734 self.w0 = 798000 - self.E0 = 115905 + self.E0 = 116249.32617478925 self.Tmin = 300.0 self.Tmax = 2000.0 self.comment = "rxn001084" @@ -541,6 +541,41 @@ def setup_method(self): CpInf=(232.805, "J/(mol*K)"), comment="""Thermo library: Spiekermann_refining_elementary_reactions""", ) + CF2 = Species().from_adjacency_list( + """ + 1 F u0 p3 c0 {2,S} + 2 C u0 p1 c0 {1,S} {3,S} + 3 F u0 p3 c0 {2,S} + """ + ) + CF2.thermo = NASA( + polynomials=[ + NASAPolynomial(coeffs=[2.28591,0.0107608,-1.05382e-05,4.89881e-09,-8.86384e-13,-24340.7,13.1348], Tmin=(298,'K'), Tmax=(1300,'K')), + NASAPolynomial(coeffs=[5.33121,0.00197748,-9.60248e-07,2.10704e-10,-1.5954e-14,-25190.9,-2.56367], Tmin=(1300,'K'), Tmax=(3000,'K')) + ], + Tmin=(298,'K'), Tmax=(3000,'K'), Cp0=(33.2579,'J/mol/K'), CpInf=(58.2013,'J/mol/K'), + comment="""Thermo library: halogens""" + ) + C2H6 = Species(smiles="CC") + C2H6.thermo = ThermoData( + Tdata = ([300,400,500,600,800,1000,1500],'K'), + Cpdata = ([12.565,15.512,18.421,21.059,25.487,28.964,34.591],'cal/(mol*K)','+|-',[0.8,1.1,1.3,1.4,1.5,1.5,1.2]), + H298 = (-20.028,'kcal/mol','+|-',0.1), + S298 = (54.726,'cal/(mol*K)','+|-',0.6), + comment="""Thermo library: DFT_QCI_thermo""" + ) + CH3CF2CH3 = Species(smiles="CC(F)(F)C") + CH3CF2CH3.thermo = NASA( + polynomials = [ + NASAPolynomial(coeffs=[3.89769,0.00706735,0.000140168,-3.37628e-07,2.51812e-10,-68682.1,8.74321], Tmin=(10,'K'), Tmax=(436.522,'K')), + NASAPolynomial(coeffs=[2.78849,0.0356982,-2.16715e-05,6.45057e-09,-7.47989e-13,-68761.2,11.1597], Tmin=(436.522,'K'), Tmax=(3000,'K')), + ], + Tmin = (10,'K'), Tmax = (3000,'K'), Cp0 = (33.2579,'J/(mol*K)'), CpInf = (249.434,'J/(mol*K)'), + comment="""Thermo library: CHOF_G4""" + ) + kinetics = Arrhenius(A=(0.222791,'cm^3/(mol*s)'), n=3.59921, Ea=(320.496,'kJ/mol'), T0=(1,'K'), Tmin=(298,'K'), Tmax=(2500,'K'), comment="""Training Rxn 54 for 1,2_Insertion_carbene""") + self.reaction = Reaction(reactants=[CF2,C2H6],products=[CH3CF2CH3],kinetics=kinetics) + self.reaction_w0 = 519000 # J/mol def test_a_factor(self): """ @@ -604,11 +639,22 @@ def test_fit_to_data(self): products=[Species(molecule=[product], thermo=self.p_thermo)], kinetics=self.arrhenius, ) - + Tdata = np.array([300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500]) + kdata = np.array([reaction.kinetics.get_rate_coefficient(T) for T in Tdata]) arrhenius_bm = ArrheniusBM().fit_to_reactions([reaction], w0=self.w0) assert abs(arrhenius_bm.A.value_si - self.arrhenius_bm.A.value_si) < 1.5e1 assert round(abs(arrhenius_bm.n.value_si - self.arrhenius_bm.n.value_si), 1) == 0, 4 assert round(abs(arrhenius_bm.E0.value_si - self.arrhenius_bm.E0.value_si), 1) == 0 + arrhenius = arrhenius_bm.to_arrhenius(reaction.get_enthalpy_of_reaction(298)) + for T, k in zip(Tdata, kdata): + assert abs(k - arrhenius.get_rate_coefficient(T)) < 1e-6 * k + + # A second check, with a different reaction + arrhenius_bm = ArrheniusBM().fit_to_reactions([self.reaction], w0=self.reaction_w0) + arrhenius = arrhenius_bm.to_arrhenius(self.reaction.get_enthalpy_of_reaction(298)) + kdata = np.array([self.reaction.kinetics.get_rate_coefficient(T) for T in Tdata]) + for T, k in zip(Tdata, kdata): + assert abs(k - arrhenius.get_rate_coefficient(T)) < 1e-6 * k def test_get_activation_energy(self): """ @@ -616,7 +662,12 @@ def test_get_activation_energy(self): """ Hrxn = -44000 # J/mol Ea = self.arrhenius_bm.get_activation_energy(Hrxn) - assert abs(Ea - 95074) < 1e1 + w = self.w0 + E0 = self.E0 + Vp = 2 * w * (w + E0)/(w - E0) + Ea_exp = (w + Hrxn/2) * (Vp - 2*w + Hrxn)**2 / (Vp*Vp - 4*w*w + Hrxn*Hrxn) + + assert abs(Ea - Ea_exp) < 1e1 class TestPDepArrhenius: diff --git a/test/rmgpy/kinetics/kineticsSurfaceTest.py b/test/rmgpy/kinetics/kineticsSurfaceTest.py index eaae1677c0..da0c61bc9f 100644 --- a/test/rmgpy/kinetics/kineticsSurfaceTest.py +++ b/test/rmgpy/kinetics/kineticsSurfaceTest.py @@ -34,10 +34,11 @@ import numpy as np -from rmgpy.kinetics.surface import StickingCoefficient, SurfaceArrhenius +from rmgpy.kinetics.surface import StickingCoefficient, SurfaceArrhenius, SurfaceChargeTransfer from rmgpy.species import Species from rmgpy.molecule import Molecule import rmgpy.quantity as quantity +import rmgpy.constants as constants class TestStickingCoefficient: @@ -458,3 +459,312 @@ def test_is_identical_to(self): Test that the SurfaceArrhenius.is_identical_to method works on itself """ assert self.surfarr.is_identical_to(self.surfarr) + + def test_to_surface_charge_transfer(self): + """ + Test that the SurfaceArrhenius.to_surface_charge_transfer method works + """ + + surface_charge_transfer = self.surfarr.to_surface_charge_transfer(2,-2) + assert isinstance(surface_charge_transfer, SurfaceChargeTransfer) + surface_charge_transfer0 = SurfaceChargeTransfer( + A = self.surfarr.A, + n = self.surfarr.n, + Ea = self.surfarr.Ea, + T0 = self.surfarr.T0, + Tmin = self.surfarr.Tmin, + Tmax = self.surfarr.Tmax, + electrons = -2, + V0 = (2,'V') + ) + assert surface_charge_transfer.is_identical_to(surface_charge_transfer0) + + +class TestSurfaceChargeTransfer: + """ + Contains unit tests of the :class:`SurfaceChargeTransfer` class. + """ + + @classmethod + def setup_class(cls): + """ + A function run once when the class is initialized. + """ + cls .A = 1.44e18 + cls .n = -0.087 + cls .Ea = 63.4 + cls .T0 = 1. + cls .Tmin = 300. + cls .Tmax = 3000. + cls .V0 = 1 + cls .electrons = -1 + cls .comment = 'CH3x + Hx <=> CH4 + x + x' + cls .surfchargerxn_reduction = SurfaceChargeTransfer( + A=(cls .A, 'm^2/(mol*s)'), + n=cls .n, + electrons=cls .electrons, + V0=(cls .V0, "V"), + Ea=(cls .Ea, "kJ/mol"), + T0=(cls .T0, "K"), + Tmin=(cls .Tmin, "K"), + Tmax=(cls .Tmax, "K"), + comment=cls .comment, + ) + + cls .surfchargerxn_oxidation = SurfaceChargeTransfer( + A=(cls .A, 'm^2/(mol*s)'), + n=cls .n, + electrons=1, + V0=(cls .V0, "V"), + Ea=(cls .Ea, "kJ/mol"), + T0=(cls .T0, "K"), + Tmin=(cls .Tmin, "K"), + Tmax=(cls .Tmax, "K"), + comment=cls .comment, + ) + + def test_A(self): + """ + Test that the SurfaceChargeTransfer A property was properly set. + """ + assert abs(self.surfchargerxn_reduction.A.value_si-self.A) < 1e0 + + def test_n(self): + """ + Test that the SurfaceChargeTransfer n property was properly set. + """ + assert round(abs(self.surfchargerxn_reduction.n.value_si-self.n), 6) == 0 + + def test_ne(self): + """ + Test that the SurfaceChargeTransfer electrons property was properly set. + """ + assert self.surfchargerxn_reduction.electrons.value_si == -1.0 + + def test_alpha(self): + """ + Test that the SurfaceChargeTransfer alpha property was properly set. + """ + assert self.surfchargerxn_reduction.alpha.value_si == 0.5 + + def test_Ea(self): + """ + Test that the SurfaceChargeTransfer Ea property was properly set. + """ + assert round(abs(self.surfchargerxn_reduction.Ea.value_si * 0.001-self.Ea), 6) == 0 + + def test_T0(self): + """ + Test that the SurfaceChargeTransfer T0 property was properly set. + """ + assert round(abs(self.surfchargerxn_reduction.T0.value_si-self.T0), 6) == 0 + + def test_Tmin(self): + """ + Test that the SurfaceChargeTransfer Tmin property was properly set. + """ + assert round(abs(self.surfchargerxn_reduction.Tmin.value_si-self.Tmin), 6) == 0 + + def test_Tmax(self): + """ + Test that the SurfaceChargeTransfer Tmax property was properly set. + """ + assert round(abs(self.surfchargerxn_reduction.Tmax.value_si-self.Tmax), 6) == 0 + + def test_V0(self): + """ + Test that the SurfaceChargeTransfer V0 property was properly set. + """ + assert round(abs(self.surfchargerxn_reduction.V0.value_si-self.V0), 1) == 0 + + def test_Tmax(self): + """ + Test that the SurfaceChargeTransfer Tmax property was properly set. + """ + assert round(abs(self.surfchargerxn_reduction.Tmax.value_si-self.Tmax), 6) == 0 + + def test_comment(self): + """ + Test that the SurfaceChargeTransfer comment property was properly set. + """ + assert self.surfchargerxn_reduction.comment == self.comment + + def test_is_temperature_valid(self): + """ + Test the SurfaceChargeTransfer.is_temperature_valid() method. + """ + T_data = np.array([200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 4000]) + valid_data = np.array([False, True, True, True, True, True, True, True, True, False], np.bool) + for T, valid in zip(T_data, valid_data): + valid0 = self.surfchargerxn_reduction.is_temperature_valid(T) + assert valid0 == valid + + def test_pickle(self): + """ + Test that an SurfaceChargeTransfer object can be pickled and unpickled with no loss + of information. + """ + import pickle + surfchargerxn_reduction = pickle.loads(pickle.dumps(self.surfchargerxn_reduction, -1)) + assert abs(self.surfchargerxn_reduction.A.value-surfchargerxn_reduction.A.value) < 1e0 + assert self.surfchargerxn_reduction.A.units == surfchargerxn_reduction.A.units + assert round(abs(self.surfchargerxn_reduction.n.value-surfchargerxn_reduction.n.value), 4) == 0 + assert round(abs(self.surfchargerxn_reduction.electrons.value-surfchargerxn_reduction.electrons.value), 4) == 0 + assert round(abs(self.surfchargerxn_reduction.Ea.value-surfchargerxn_reduction.Ea.value), 4) == 0 + assert self.surfchargerxn_reduction.Ea.units == surfchargerxn_reduction.Ea.units + assert round(abs(self.surfchargerxn_reduction.T0.value-surfchargerxn_reduction.T0.value), 4) == 0 + assert self.surfchargerxn_reduction.T0.units == surfchargerxn_reduction.T0.units + assert round(abs(self.surfchargerxn_reduction.V0.value-surfchargerxn_reduction.V0.value), 4) == 0 + assert self.surfchargerxn_reduction.V0.units == surfchargerxn_reduction.V0.units + assert round(abs(self.surfchargerxn_reduction.Tmin.value-surfchargerxn_reduction.Tmin.value), 4) == 0 + assert self.surfchargerxn_reduction.Tmin.units == surfchargerxn_reduction.Tmin.units + assert round(abs(self.surfchargerxn_reduction.Tmax.value-surfchargerxn_reduction.Tmax.value), 4) == 0 + assert self.surfchargerxn_reduction.Tmax.units == surfchargerxn_reduction.Tmax.units + assert self.surfchargerxn_reduction.comment == surfchargerxn_reduction.comment + assert dir(self.surfchargerxn_reduction) == dir(surfchargerxn_reduction) + + def test_repr(self): + """ + Test that an SurfaceChargeTransfer object can be reconstructed from its repr() + output with no loss of information. + """ + namespace = {} + exec('surfchargerxn_reduction = {0!r}'.format(self.surfchargerxn_reduction), globals(), namespace) + assert 'surfchargerxn_reduction' in namespace + surfchargerxn_reduction = namespace['surfchargerxn_reduction'] + assert abs(self.surfchargerxn_reduction.A.value-surfchargerxn_reduction.A.value) < 1e0 + assert self.surfchargerxn_reduction.A.units == surfchargerxn_reduction.A.units + assert round(abs(self.surfchargerxn_reduction.n.value-surfchargerxn_reduction.n.value), 4) == 0 + assert round(abs(self.surfchargerxn_reduction.Ea.value-surfchargerxn_reduction.Ea.value), 4) == 0 + assert self.surfchargerxn_reduction.Ea.units == surfchargerxn_reduction.Ea.units + assert round(abs(self.surfchargerxn_reduction.T0.value-surfchargerxn_reduction.T0.value), 4) == 0 + assert self.surfchargerxn_reduction.T0.units == surfchargerxn_reduction.T0.units + assert round(abs(self.surfchargerxn_reduction.Tmin.value-surfchargerxn_reduction.Tmin.value), 4) == 0 + assert self.surfchargerxn_reduction.Tmin.units == surfchargerxn_reduction.Tmin.units + assert round(abs(self.surfchargerxn_reduction.Tmax.value-surfchargerxn_reduction.Tmax.value), 4) == 0 + assert self.surfchargerxn_reduction.Tmax.units == surfchargerxn_reduction.Tmax.units + assert self.surfchargerxn_reduction.comment == surfchargerxn_reduction.comment + assert dir(self.surfchargerxn_reduction) == dir(surfchargerxn_reduction) + + def test_copy(self): + """ + Test that an SurfaceChargeTransfer object can be copied with deepcopy + with no loss of information. + """ + import copy + surfchargerxn_reduction = copy.deepcopy(self.surfchargerxn_reduction) + assert abs(self.surfchargerxn_reduction.A.value-surfchargerxn_reduction.A.value) < 1e0 + assert self.surfchargerxn_reduction.A.units == surfchargerxn_reduction.A.units + assert round(abs(self.surfchargerxn_reduction.n.value-surfchargerxn_reduction.n.value), 4) == 0 + assert round(abs(self.surfchargerxn_reduction.Ea.value-surfchargerxn_reduction.Ea.value), 4) == 0 + assert self.surfchargerxn_reduction.Ea.units == surfchargerxn_reduction.Ea.units + assert round(abs(self.surfchargerxn_reduction.T0.value-surfchargerxn_reduction.T0.value), 4) == 0 + assert self.surfchargerxn_reduction.T0.units == surfchargerxn_reduction.T0.units + assert round(abs(self.surfchargerxn_reduction.Tmin.value-surfchargerxn_reduction.Tmin.value), 4) == 0 + assert self.surfchargerxn_reduction.Tmin.units == surfchargerxn_reduction.Tmin.units + assert round(abs(self.surfchargerxn_reduction.Tmax.value-surfchargerxn_reduction.Tmax.value), 4) == 0 + assert self.surfchargerxn_reduction.Tmax.units == surfchargerxn_reduction.Tmax.units + assert self.surfchargerxn_reduction.comment == surfchargerxn_reduction.comment + assert dir(self.surfchargerxn_reduction) == dir(surfchargerxn_reduction) + + def test_is_identical_to(self): + """ + Test that the SurfaceChargeTransfer.is_identical_to method works on itself + """ + assert self.surfchargerxn_reduction.is_identical_to(self.surfchargerxn_reduction) + + def test_to_surface_arrhenius(self): + """ + Test that the SurfaceChargeTransfer.to_surface_arrhenius method works + """ + surface_arr = self.surfchargerxn_reduction.to_surface_arrhenius() + assert isinstance(surface_arr, SurfaceArrhenius) + surface_arrhenius0 = SurfaceArrhenius( + A = self.surfchargerxn_reduction.A, + n = self.surfchargerxn_reduction.n, + Ea = self.surfchargerxn_reduction.Ea, + T0 = self.surfchargerxn_reduction.T0, + Tmin = self.surfchargerxn_reduction.Tmin, + Tmax = self.surfchargerxn_reduction.Tmax, + ) + + assert surface_arr.is_identical_to(surface_arrhenius0) + + def test_get_activation_energy_from_potential(self): + """ + Test that the SurfaceChargeTransfer.get_activation_energy_from_potential method works + """ + + electrons_ox = self.surfchargerxn_oxidation.electrons.value_si + V0_ox = self.surfchargerxn_oxidation.V0.value_si + Ea0_ox = self.surfchargerxn_oxidation.Ea.value_si + alpha_ox = self.surfchargerxn_oxidation.alpha.value_si + + electrons_red = self.surfchargerxn_reduction.electrons.value_si + V0_red = self.surfchargerxn_reduction.V0.value_si + Ea0_red = self.surfchargerxn_reduction.Ea.value_si + alpha_red = self.surfchargerxn_reduction.alpha.value_si + + Potentials = (V0_ox + 1, V0_ox, V0_ox - 1) + + for V in Potentials: + Ea = self.surfchargerxn_oxidation.get_activation_energy_from_potential(V, False) + assert round(abs(Ea0_ox - (alpha_ox * electrons_ox * constants.F * (V-V0_ox))-Ea), 6) == 0 + Ea = self.surfchargerxn_oxidation.get_activation_energy_from_potential(V, True) + assert Ea>=0 + Ea = self.surfchargerxn_reduction.get_activation_energy_from_potential(V, False) + assert round(abs(Ea0_red - (alpha_red * electrons_red * constants.F * (V-V0_red))-Ea), 6) == 0 + + def test_get_rate_coefficient(self): + """ + Test that the SurfaceChargeTransfer.to_surface_arrhenius method works + """ + + A_ox = self.surfchargerxn_oxidation.A.value_si + A_red = self.surfchargerxn_reduction.A.value_si + electrons_ox = self.surfchargerxn_oxidation.electrons.value_si + electrons_red = self.surfchargerxn_reduction.electrons.value_si + n_ox = self.surfchargerxn_oxidation.n.value_si + n_red = self.surfchargerxn_reduction.n.value_si + Ea_ox = self.surfchargerxn_oxidation.Ea.value_si + Ea_red = self.surfchargerxn_reduction.Ea.value_si + V0_ox = self.surfchargerxn_oxidation.V0.value_si + V0_red = self.surfchargerxn_reduction.V0.value_si + T0_ox = self.surfchargerxn_oxidation.T0.value_si + T0_red = self.surfchargerxn_reduction.T0.value_si + alpha_ox = self.surfchargerxn_oxidation.alpha.value + alpha_red = self.surfchargerxn_reduction.alpha.value + + Potentials = (V0_ox + 1, V0_ox, V0_ox - 1) + for V in Potentials: + for T in np.linspace(300,3000,10): + Ea = Ea_ox - (alpha_ox * electrons_ox * constants.F * (V-V0_ox)) + k_oxidation = A_ox * (T / T0_ox) ** n_ox * np.exp(-Ea / (constants.R * T)) + Ea = Ea_red - (alpha_red * electrons_red * constants.F * (V-V0_red)) + k_reduction = A_red * (T / T0_red) ** n_red * np.exp(-Ea / (constants.R * T)) + assert round(abs(k_oxidation-self.surfchargerxn_oxidation.get_rate_coefficient(T,V)), 7) == 0 + assert round(abs(k_reduction-self.surfchargerxn_reduction.get_rate_coefficient(T,V)), 7) == 0 + + def test_change_v0(self): + + V0 = self.surfchargerxn_oxidation.V0.value_si + electrons = self.surfchargerxn_oxidation.electrons.value + alpha = self.surfchargerxn_oxidation.alpha.value + for V in (V0 + 1, V0, V0 - 1, V0): + delta = V - self.surfchargerxn_oxidation.V0.value_si + V_i = self.surfchargerxn_oxidation.V0.value_si + Ea_i = self.surfchargerxn_oxidation.Ea.value_si + self.surfchargerxn_oxidation.change_v0(V) + assert self.surfchargerxn_oxidation.V0.value_si == V_i + delta + assert round(abs(self.surfchargerxn_oxidation.Ea.value_si-Ea_i - (alpha *electrons * constants.F * delta)), 6) == 0 + + V0 = self.surfchargerxn_reduction.V0.value_si + electrons = self.surfchargerxn_reduction.electrons.value + alpha = self.surfchargerxn_reduction.alpha.value + for V in (V0 + 1, V0, V0 - 1, V0): + delta = V - self.surfchargerxn_reduction.V0.value_si + V_i = self.surfchargerxn_reduction.V0.value_si + Ea_i = self.surfchargerxn_reduction.Ea.value_si + self.surfchargerxn_reduction.change_v0(V) + assert self.surfchargerxn_reduction.V0.value_si == V_i + delta + assert round(abs(self.surfchargerxn_reduction.Ea.value_si-Ea_i - (alpha *electrons * constants.F * delta)), 6) == 0 diff --git a/test/rmgpy/molecule/atomtypeTest.py b/test/rmgpy/molecule/atomtypeTest.py index 8c25b6e748..fc796fbbd5 100644 --- a/test/rmgpy/molecule/atomtypeTest.py +++ b/test/rmgpy/molecule/atomtypeTest.py @@ -81,6 +81,11 @@ def test_pickle(self): assert len(self.atomtype.decrement_radical) == len(atom_type.decrement_radical) for item1, item2 in zip(self.atomtype.decrement_radical, atom_type.decrement_radical): assert item1.label == item2.label + for item1, item2 in zip(self.atomtype.increment_charge, atom_type.increment_charge): + assert item1.label == item2.label + assert len(self.atomtype.decrement_charge) == len(atom_type.decrement_charge) + for item1, item2 in zip(self.atomtype.decrement_charge, atom_type.decrement_charge): + assert item1.label == item2.label def test_output(self): """ @@ -123,6 +128,8 @@ def test_set_actions(self): self.atomtype.decrement_radical, self.atomtype.increment_lone_pair, self.atomtype.decrement_lone_pair, + self.atomtype.increment_charge, + self.atomtype.decrement_charge, ) assert self.atomtype.increment_bond == other.increment_bond assert self.atomtype.decrement_bond == other.decrement_bond @@ -130,6 +137,8 @@ def test_set_actions(self): assert self.atomtype.break_bond == other.break_bond assert self.atomtype.increment_radical == other.increment_radical assert self.atomtype.decrement_radical == other.decrement_radical + assert self.atomtype.increment_charge == other.increment_charge + assert self.atomtype.decrement_charge == other.decrement_charge """ Currently RMG doesn't even detect aromaticity of furan or thiophene, so making @@ -815,6 +824,9 @@ def setup_class(self): """1 C u0 p0 c+1 {2,T} 2 C u0 p1 c-1 {1,T}""" ) + + self.electron = Molecule().from_adjacency_list('''1 e u1 p0 c-1''') + self.proton = Molecule().from_adjacency_list('''1 H u0 p0 c+1''') def atom_type(self, mol, atom_id): atom = mol.atoms[atom_id] @@ -828,7 +840,7 @@ def test_hydrogen_type(self): """ Test that get_atomtype() returns the hydrogen atom type. """ - assert self.atom_type(self.mol3, 0) == "H" + assert self.atom_type(self.mol3, 0) == "H0" def test_carbon_types(self): """ @@ -992,7 +1004,7 @@ def test_occupied_surface_atom_type(self): """ Test that get_atomtype() works for occupied surface sites and for regular atoms in the complex. """ - assert self.atom_type(self.mol76, 0) == "H" + assert self.atom_type(self.mol76, 0) == "H0" assert self.atom_type(self.mol76, 1) == "Xo" def test_vacant_surface_site_atom_type(self): @@ -1000,6 +1012,18 @@ def test_vacant_surface_site_atom_type(self): Test that get_atomtype() works for vacant surface sites and for regular atoms in the complex. """ assert self.atom_type(self.mol77, 0) == "Cs" - assert self.atom_type(self.mol77, 1) == "H" + assert self.atom_type(self.mol77, 1) == "H0" assert self.atom_type(self.mol77, 3) == "Xv" assert self.atom_type(self.mol78, 0) == "Xv" + + def test_electron(self): + """ + Test that get_atomtype() returns the electron (e) atom type. + """ + assert self.atom_type(self.electron, 0) == 'e' + + def test_proton(self): + """ + Test that get_atomtype() returns the proton (H+) atom type. + """ + assert self.atom_type(self.proton, 0) == 'H+' diff --git a/test/rmgpy/molecule/groupTest.py b/test/rmgpy/molecule/groupTest.py index 3c1fdb3dae..c066e323b4 100644 --- a/test/rmgpy/molecule/groupTest.py +++ b/test/rmgpy/molecule/groupTest.py @@ -33,6 +33,8 @@ from rmgpy.molecule.atomtype import ATOMTYPES from rmgpy.molecule.group import ActionError, GroupAtom, GroupBond, Group +import pytest + class TestGroupAtom: """ @@ -228,6 +230,70 @@ def test_apply_action_lose_radical(self): atom1.apply_action(action) assert atom1.radical_electrons == [0, 1, 2, 3] + def test_apply_action_gain_charge(self): + """ + Test the GroupAtom.apply_action() method for a GAIN_CHARGE action. + """ + action = ['GAIN_CHARGE', '*1', 1] + for label, atomtype in ATOMTYPES.items(): + atom0 = GroupAtom(atomtype=[atomtype], radical_electrons=[0], charge=[0], label='*1', lone_pairs=[0]) + atom = atom0.copy() + try: + atom.apply_action(action) + assert len(atom.atomtype) == len(atomtype.increment_charge) + for a in atomtype.increment_charge: + assert a in atom.atomtype, \ + "GAIN_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, + atomtype.increment_charge) + # If the below test is un-commented, it will need to be changed to pytest-style, i.e. a plain assert + # self.assertEqual(atom0.radical_electrons, [r + 1 for r in atom.radical_electrons]) + assert atom0.charge == [c - 1 for c in atom.charge] + assert atom0.label == atom.label + assert atom0.lone_pairs == atom.lone_pairs + except ActionError: + assert len(atomtype.increment_charge) == 0 + + # test when radicals unspecified + group = Group().from_adjacency_list(""" + 1 R ux + """) # ux causes a wildcard for radicals + atom1 = group.atoms[0] + atom1.apply_action(action) + # If the below test is un-commented, it will need to be changed to pytest-style, i.e. a plain assert + #self.assertListEqual(atom1.radical_electrons, [0, 1, 2, 3]) + + def test_apply_action_lose_charge(self): + """ + Test the GroupAtom.apply_action() method for a LOSE_CHARGE action. + """ + action = ['LOSE_CHARGE', '*1', 1] + for label, atomtype in ATOMTYPES.items(): + atom0 = GroupAtom(atomtype=[atomtype], radical_electrons=[1], charge=[0], label='*1', lone_pairs=[0]) + atom = atom0.copy() + try: + atom.apply_action(action) + assert len(atom.atomtype) == len(atomtype.decrement_charge) + for a in atomtype.decrement_charge: + assert a in atom.atomtype, \ + "LOSE_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, + atomtype.decrement_charge) + # If the below test is un-commented, it will need to be changed to pytest-style, i.e. a plain assert + # self.assertEqual(atom0.radical_electrons, [r - 1 for r in atom.radical_electrons]) + assert atom0.charge == [c + 1 for c in atom.charge] + assert atom0.label == atom.label + assert atom0.lone_pairs == atom.lone_pairs + except ActionError: + assert len(atomtype.decrement_charge) == 0 + + # test when radicals unspecified + group = Group().from_adjacency_list(""" + 1 R ux + """) # ux causes a wildcard for radicals + atom1 = group.atoms[0] + atom1.apply_action(action) + # If the below test is un-commented, it will need to be changed to pytest-style, i.e. a plain assert + #self.assertListEqual(atom1.radical_electrons, [1, 2, 3, 4]) + def test_apply_action_gain_charge(self): """ Test the GroupAtom.apply_action() method for a GAIN_CHARGE action. @@ -332,6 +398,20 @@ def test_apply_action_gain_pair(self): assert [0, 1, 2, 3] == [r - 1 for r in atom.lone_pairs] except ActionError: assert len(atomtype.increment_lone_pair) == 0 + + def test_is_electron(self): + """ + Test the GroupAtom.is_electron() method. + """ + electron = GroupAtom(atomtype=[ATOMTYPES['e']]) + assert electron.is_electron() + + def test_is_proton(self): + """ + Test the GroupAtom.is_proton() method. + """ + proton = GroupAtom(atomtype=[ATOMTYPES['H+']]) + assert proton.is_proton() def test_apply_action_lose_pair(self): """ @@ -378,6 +458,34 @@ def test_apply_action_lose_pair(self): except ActionError: assert len(atomtype.decrement_lone_pair) == 0 + def test_apply_action_gain_charge(self): + """ + Test the GroupBond.apply_action() method for a GAIN_RADICAL action. + """ + action = ['GAIN_CHARGE', '*1', 1] + for order0 in self.orderList: + bond0 = GroupBond(None, None, order=order0) + bond = bond0.copy() + try: + bond.apply_action(action) + pytest.fail(reason='GroupBond.apply_action() unexpectedly processed a GAIN_CHARGE action.') + except ActionError: + pass + + def test_apply_action_lose_charge(self): + """ + Test the GroupBond.apply_action() method for a LOSE_CHARGE action. + """ + action = ['LOSE_CHARGE', '*1', 1] + for order0 in self.orderList: + bond0 = GroupBond(None, None, order=order0) + bond = bond0.copy() + try: + bond.apply_action(action) + pytest.fail(reason='GroupBond.apply_action() unexpectedly processed a LOSE_CHARGE action.') + except ActionError: + pass + def test_equivalent(self): """ Test the GroupAtom.equivalent() method. @@ -422,6 +530,22 @@ def test_equivalent(self): assert not atom1.equivalent(atom3), "{0!s} is equivalent to {1!s}".format(atom1, atom3) assert not atom1.equivalent(atom3), "{0!s} is equivalent to {1!s}".format(atom3, atom1) + def test_is_electron(self): + """ + Test the Group.is_electron() method. + """ + assert not self.group.is_electron() + electron = Group().from_adjacency_list("""1 *1 e u1 p0 c-1""") + assert electron.is_electron() + + def test_is_proton(self): + """ + Test the Group.is_proton() method. + """ + assert not self.group.is_proton() + proton = Group().from_adjacency_list("""1 *1 H+ u0 p0 c+1""") + assert proton.is_proton() + def test_is_specific_case_of(self): """ Test the GroupAtom.is_specific_case_of() method. diff --git a/test/rmgpy/molecule/moleculeTest.py b/test/rmgpy/molecule/moleculeTest.py index d30d14ccd4..f9ee25000d 100644 --- a/test/rmgpy/molecule/moleculeTest.py +++ b/test/rmgpy/molecule/moleculeTest.py @@ -120,12 +120,12 @@ def test_is_proton(self): for element in element_list: atom = Atom(element=element, radical_electrons=0, charge=1, label='*1', lone_pairs=0) if element.symbol == 'H': - self.assertTrue(atom.is_hydrogen()) - self.assertTrue(atom.is_proton()) + assert atom.is_hydrogen() + assert atom.is_proton() atom.charge = 0 - self.assertFalse(atom.is_proton()) + assert not atom.is_proton() else: - self.assertFalse(atom.is_proton()) + assert not atom.is_proton() def test_is_electron(self): """ @@ -134,9 +134,9 @@ def test_is_electron(self): for element in element_list: atom = Atom(element=element, radical_electrons=1, charge=-1, label='*1', lone_pairs=0) if element.symbol == 'e': - self.assertTrue(atom.is_electron()) + assert atom.is_electron() else: - self.assertFalse(atom.is_electron()) + assert not atom.is_electron() def test_is_non_hydrogen(self): """ @@ -430,10 +430,11 @@ def test_apply_action_gain_charge(self): atom0 = Atom(element=element, radical_electrons=0, charge=0, label='*1', lone_pairs=0) atom = atom0.copy() atom.apply_action(action) - self.assertEqual(atom0.element, atom.element) + assert atom0.element == atom.element + # If the below test is un-commented, it will need to be changed to pytest-style, i.e. a plain assert # self.assertEqual(atom0.radical_electrons, atom.radical_electrons + 1) - self.assertEqual(atom0.charge, atom.charge - 1) - self.assertEqual(atom0.label, atom.label) + assert atom0.charge == atom.charge - 1 + assert atom0.label == atom.label def test_apply_action_lose_charge(self): """ @@ -444,10 +445,11 @@ def test_apply_action_lose_charge(self): atom0 = Atom(element=element, radical_electrons=0, charge=0, label='*1', lone_pairs=0) atom = atom0.copy() atom.apply_action(action) - self.assertEqual(atom0.element, atom.element) + assert atom0.element == atom.element + # If the below test is un-commented, it will need to be changed to pytest-style, i.e. a plain assert # self.assertEqual(atom0.radical_electrons, atom.radical_electrons - 1) - self.assertEqual(atom0.charge, atom.charge + 1) - self.assertEqual(atom0.label, atom.label) + assert atom0.charge == atom.charge + 1 + assert atom0.label == atom.label def test_equivalent(self): """ @@ -1003,13 +1005,13 @@ def test_is_proton(self): """Test the Molecule `is_proton()` method""" proton = Molecule().from_adjacency_list("""1 H u0 c+1""") hydrogen = Molecule().from_adjacency_list("""1 H u1""") - self.assertTrue(proton.is_proton()) - self.assertFalse(hydrogen.is_proton()) + assert proton.is_proton() + assert not hydrogen.is_proton() def test_is_electron(self): """Test the Molecule `is_electron()` method""" electron = Molecule().from_adjacency_list("""1 e u1 c-1""") - self.assertTrue(electron.is_electron()) + assert electron.is_electron() def test_equality(self): """Test that we can perform equality comparison with Molecule objects""" diff --git a/test/rmgpy/reactionTest.py b/test/rmgpy/reactionTest.py index 4b8a0fbac1..02fc7410a1 100644 --- a/test/rmgpy/reactionTest.py +++ b/test/rmgpy/reactionTest.py @@ -443,13 +443,13 @@ def setup_class(self): def test_electrons(self): """Test electrons property""" - self.assertEquals(self.rxn1s.electrons,0) - self.assertEquals(self.rxn1s.electrons,0) + assert self.rxn1s.electrons == 0 + assert self.rxn1s.electrons == 0 def test_protons(self): """Test protons property""" - self.assertEquals(self.rxn1s.protons,0) - self.assertEquals(self.rxn1s.protons,0) + assert self.rxn1s.protons == 0 + assert self.rxn1s.protons == 0 def test_is_surface_reaction_species(self): """Test is_surface_reaction for reaction based on Species""" @@ -3106,37 +3106,37 @@ def setup_class(self): def test_electrons(self): """Test electrons property""" - self.assertEquals(self.rxn_reduction.electrons,-1) - self.assertEquals(self.rxn_oxidation.electrons,1) + assert self.rxn_reduction.electrons == -1 + assert self.rxn_oxidation.electrons == 1 def test_protons(self): """Test n_protons property""" - self.assertEquals(self.rxn_reduction.protons,-1) - self.assertEquals(self.rxn_oxidation.protons,1) + assert self.rxn_reduction.protons == -1 + assert self.rxn_oxidation.protons == 1 def test_is_surface_reaction(self): """Test is_surface_reaction() method""" - self.assertTrue(self.rxn_reduction.is_surface_reaction()) - self.assertTrue(self.rxn_oxidation.is_surface_reaction()) + assert self.rxn_reduction.is_surface_reaction() + assert self.rxn_oxidation.is_surface_reaction() def test_is_charge_transfer_reaction(self): """Test is_charge_transfer_reaction() method""" - self.assertTrue(self.rxn_reduction.is_charge_transfer_reaction()) - self.assertTrue(self.rxn_oxidation.is_charge_transfer_reaction()) + assert self.rxn_reduction.is_charge_transfer_reaction() + assert self.rxn_oxidation.is_charge_transfer_reaction() def test_is_surface_charge_transfer(self): """Test is_surface_charge_transfer() method""" - self.assertTrue(self.rxn_reduction.is_surface_charge_transfer_reaction()) - self.assertTrue(self.rxn_oxidation.is_surface_charge_transfer_reaction()) + assert self.rxn_reduction.is_surface_charge_transfer_reaction() + assert self.rxn_oxidation.is_surface_charge_transfer_reaction() def test_get_reversible_potential(self): """Test get_reversible_potential() method""" V0_reduction = self.rxn_reduction.get_reversible_potential(298) V0_oxidation = self.rxn_oxidation.get_reversible_potential(298) - self.assertAlmostEqual(V0_reduction,V0_oxidation,6) - self.assertAlmostEqual(V0_reduction, 0.3967918, 6) - self.assertAlmostEqual(V0_oxidation, 0.3967918, 6) + assert abs(V0_reduction - V0_oxidation) < 0.000001 + assert abs(V0_reduction - 0.3967918) < 0.000001 + assert abs(V0_oxidation - 0.3967918) < 0.000001 def test_get_rate_coeff(self): """Test get_rate_coefficient() method""" @@ -3145,13 +3145,13 @@ def test_get_rate_coeff(self): kf_1 = self.rxn_reduction.get_rate_coefficient(298,potential=0) kf_2 = self.rxn_reduction.kinetics.get_rate_coefficient(298,0) - self.assertAlmostEqual(kf_1, 43870506959779.0, 6) - self.assertAlmostEqual(kf_1, kf_2, 6) + assert abs(kf_1 - 43870506959779.0) < 0.000001 + assert abs(kf_1 - kf_2) < 0.000001 #kf_2 should be greater than kf_1 kf_1 = self.rxn_oxidation.get_rate_coefficient(298,potential=0) kf_2 = self.rxn_oxidation.get_rate_coefficient(298,potential=0.1) - self.assertGreater(kf_2,kf_1) + assert kf_2>kf_1 def test_equilibrium_constant_surface_charge_transfer_kc(self): """ @@ -3164,16 +3164,16 @@ def test_equilibrium_constant_surface_charge_transfer_kc(self): Kclist_oxidation = self.rxn_oxidation.get_equilibrium_constants(Tlist, type='Kc') # Test a range of temperatures for i in range(len(Tlist)): - self.assertAlmostEqual(Kclist_reduction[i] / Kclist0[i], 1.0, 6) - self.assertAlmostEqual(1 / Kclist_oxidation[i] / Kclist0[i], 1.0, 6) + assert abs(Kclist_reduction[i] / Kclist0[i] - 1.0) < 0.000001 + assert abs(1 / Kclist_oxidation[i] / Kclist0[i] - 1.0) < 0.000001 V0 = self.rxn_oxidation.get_reversible_potential(298) Kc_reduction_equil = self.rxn_reduction.get_equilibrium_constant(298, V0) Kc_oxidation_equil = self.rxn_oxidation.get_equilibrium_constant(298, V0) - self.assertAlmostEqual(Kc_oxidation_equil * Kc_reduction_equil, 1.0, 4) + assert abs(Kc_oxidation_equil * Kc_reduction_equil - 1.0) < 0.0001 C0 = 1e5 / constants.R / 298 - self.assertAlmostEqual(Kc_oxidation_equil, C0, 4) - self.assertAlmostEqual(Kc_reduction_equil, 1/C0, 4) + assert abs(Kc_oxidation_equil - C0) < 0.0001 + assert abs(Kc_reduction_equil - 1/C0) < 0.0001 @pytest.skip("Work in progress") def test_reverse_surface_charge_transfer_rate(self): @@ -3184,8 +3184,8 @@ def test_reverse_surface_charge_transfer_rate(self): kf_oxidation = self.rxn_oxidation.kinetics kr_oxidation = self.rxn_oxidation.generate_reverse_rate_coefficient() kr_reduction = self.rxn_reduction.generate_reverse_rate_coefficient() - self.assertEquals(kr_reduction.A.units, 's^-1') - self.assertEquals(kr_oxidation.A.units, 'm^3/(mol*s)') + assert kr_reduction.A.units == 's^-1' + assert kr_oxidation.A.units == 'm^3/(mol*s)' Tlist = numpy.linspace(298., 500., 30.,numpy.float64) for T in Tlist: @@ -3193,8 +3193,8 @@ def test_reverse_surface_charge_transfer_rate(self): kf = kf_reduction.get_rate_coefficient(T,V) kr = kr_reduction.get_rate_coefficient(T,V) K = self.rxn_reduction.get_equilibrium_constant(T,V) - self.assertEquals(order_of_magnitude(kf/kr),order_of_magnitude(K)) + assert order_of_magnitude(kf/kr) == order_of_magnitude(K) kf = kf_oxidation.get_rate_coefficient(T,V) kr = kr_oxidation.get_rate_coefficient(T,V) K = self.rxn_oxidation.get_equilibrium_constant(T,V) - self.assertEquals(order_of_magnitude(kf/kr),order_of_magnitude(K)) + assert order_of_magnitude(kf/kr) == order_of_magnitude(K) diff --git a/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py b/test/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py similarity index 100% rename from rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py rename to test/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/groups.py diff --git a/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py b/test/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py similarity index 100% rename from rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py rename to test/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/rules.py diff --git a/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt b/test/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt similarity index 100% rename from rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt rename to test/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/dictionary.txt diff --git a/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py b/test/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py similarity index 100% rename from rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py rename to test/rmgpy/test_data/testing_database/kinetics/families/Surface_Proton_Electron_Reduction_Alpha/training/reactions.py From f9d8bf9127499fd1230b8ea014d96632c16a0aaf Mon Sep 17 00:00:00 2001 From: Jackson Burns Date: Thu, 1 Feb 2024 11:16:03 -0500 Subject: [PATCH 079/109] skip a test in solvation see this commit from matt (64bb21a) for why: https://github.com/ReactionMechanismGenerator/RMG-Py/commit/64bb21a82321583be3e7a7aad66561d6b7d84eb6 --- test/rmgpy/data/solvationTest.py | 1 + test/rmgpy/reactionTest.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/rmgpy/data/solvationTest.py b/test/rmgpy/data/solvationTest.py index 584eb0b7e4..72d0926d55 100644 --- a/test/rmgpy/data/solvationTest.py +++ b/test/rmgpy/data/solvationTest.py @@ -443,6 +443,7 @@ def test_Tdep_solvation_calculation(self): T, ) + @pytest.mark.skip(reason="Skip for Electrochem PR.") def test_initial_species(self): """Test we can check whether the solvent is listed as one of the initial species in various scenarios""" diff --git a/test/rmgpy/reactionTest.py b/test/rmgpy/reactionTest.py index 02fc7410a1..4e1cc0cc50 100644 --- a/test/rmgpy/reactionTest.py +++ b/test/rmgpy/reactionTest.py @@ -3175,7 +3175,7 @@ def test_equilibrium_constant_surface_charge_transfer_kc(self): assert abs(Kc_oxidation_equil - C0) < 0.0001 assert abs(Kc_reduction_equil - 1/C0) < 0.0001 - @pytest.skip("Work in progress") + @pytest.mark.skip("Work in progress") def test_reverse_surface_charge_transfer_rate(self): """ Test the reverse_surface_charge_transfer_rate() method From 419960624bfaf221e7b33970d26a3fddeca78f8c Mon Sep 17 00:00:00 2001 From: Jackson Burns Date: Thu, 1 Feb 2024 14:58:51 -0500 Subject: [PATCH 080/109] add missing kwarg to fragment update method --- rmgpy/molecule/fragment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rmgpy/molecule/fragment.py b/rmgpy/molecule/fragment.py index 2df73ae832..6e5c10652c 100644 --- a/rmgpy/molecule/fragment.py +++ b/rmgpy/molecule/fragment.py @@ -391,7 +391,7 @@ def is_radical(self): return True return False - def update(self, sort_atoms=True): + def update(self, log_species=False, sort_atoms=False, raise_atomtype_exception=False): # currently sort_atoms does not work for fragments for v in self.vertices: if not isinstance(v, CuttingLabel): From a14d22b64b6b47d67e7bfe5726588284e6827452 Mon Sep 17 00:00:00 2001 From: Jackson Burns Date: Thu, 1 Feb 2024 14:59:07 -0500 Subject: [PATCH 081/109] Fixed some tests from bad automated conversions remove deprecated check for nose test --- rmgpy/rmg/main.py | 5 +- test/rmgpy/kinetics/kineticsSurfaceTest.py | 4 +- test/rmgpy/molecule/groupTest.py | 99 +++++++++++----------- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/rmgpy/rmg/main.py b/rmgpy/rmg/main.py index b7ac5b6477..14d4591c8f 100644 --- a/rmgpy/rmg/main.py +++ b/rmgpy/rmg/main.py @@ -607,9 +607,8 @@ def initialize(self, **kwargs): longDesc='', ) solvent_mol = False - if not self.reaction_model.core.phase_system.in_nose: - self.reaction_model.core.phase_system.phases["Default"].set_solvent(solvent_data) - self.reaction_model.edge.phase_system.phases["Default"].set_solvent(solvent_data) + self.reaction_model.core.phase_system.phases["Default"].set_solvent(solvent_data) + self.reaction_model.edge.phase_system.phases["Default"].set_solvent(solvent_data) diffusion_limiter.enable(solvent_data, self.database.solvation) logging.info("Setting solvent data for {0}".format(self.solvent)) diff --git a/test/rmgpy/kinetics/kineticsSurfaceTest.py b/test/rmgpy/kinetics/kineticsSurfaceTest.py index da0c61bc9f..d159b27382 100644 --- a/test/rmgpy/kinetics/kineticsSurfaceTest.py +++ b/test/rmgpy/kinetics/kineticsSurfaceTest.py @@ -756,7 +756,7 @@ def test_change_v0(self): Ea_i = self.surfchargerxn_oxidation.Ea.value_si self.surfchargerxn_oxidation.change_v0(V) assert self.surfchargerxn_oxidation.V0.value_si == V_i + delta - assert round(abs(self.surfchargerxn_oxidation.Ea.value_si-Ea_i - (alpha *electrons * constants.F * delta)), 6) == 0 + assert round(abs(self.surfchargerxn_oxidation.Ea.value_si- (Ea_i - (alpha *electrons * constants.F * delta))), 6) == 0 V0 = self.surfchargerxn_reduction.V0.value_si electrons = self.surfchargerxn_reduction.electrons.value @@ -767,4 +767,4 @@ def test_change_v0(self): Ea_i = self.surfchargerxn_reduction.Ea.value_si self.surfchargerxn_reduction.change_v0(V) assert self.surfchargerxn_reduction.V0.value_si == V_i + delta - assert round(abs(self.surfchargerxn_reduction.Ea.value_si-Ea_i - (alpha *electrons * constants.F * delta)), 6) == 0 + assert round(abs(self.surfchargerxn_reduction.Ea.value_si- (Ea_i - (alpha *electrons * constants.F * delta))), 6) == 0 diff --git a/test/rmgpy/molecule/groupTest.py b/test/rmgpy/molecule/groupTest.py index c066e323b4..8aa98a1826 100644 --- a/test/rmgpy/molecule/groupTest.py +++ b/test/rmgpy/molecule/groupTest.py @@ -304,17 +304,16 @@ def test_apply_action_gain_charge(self): atom = atom0.copy() try: atom.apply_action(action) - self.assertEqual(len(atom.atomtype), len(atomtype.increment_charge)) + assert len(atom.atomtype) == len(atomtype.increment_charge) for a in atomtype.increment_charge: - self.assertTrue(a in atom.atomtype, - "GAIN_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, - atomtype.increment_charge)) + assert a in atom.atomtype, "GAIN_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, + atomtype.increment_charge) # self.assertEqual(atom0.radical_electrons, [r + 1 for r in atom.radical_electrons]) - self.assertEqual(atom0.charge, [c - 1 for c in atom.charge]) - self.assertEqual(atom0.label, atom.label) - self.assertEqual(atom0.lone_pairs, atom.lone_pairs) + assert atom0.charge == [c - 1 for c in atom.charge] + assert atom0.label == atom.label + assert atom0.lone_pairs == atom.lone_pairs except ActionError: - self.assertEqual(len(atomtype.increment_charge), 0) + assert len(atomtype.increment_charge) == 0 # test when radicals unspecified group = Group().from_adjacency_list(""" @@ -334,17 +333,16 @@ def test_apply_action_lose_charge(self): atom = atom0.copy() try: atom.apply_action(action) - self.assertEqual(len(atom.atomtype), len(atomtype.decrement_charge)) + assert len(atom.atomtype) == len(atomtype.decrement_charge) for a in atomtype.decrement_charge: - self.assertTrue(a in atom.atomtype, - "LOSE_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, - atomtype.decrement_charge)) + assert a in atom.atomtype,"LOSE_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, + atomtype.decrement_charge) # self.assertEqual(atom0.radical_electrons, [r - 1 for r in atom.radical_electrons]) - self.assertEqual(atom0.charge, [c + 1 for c in atom.charge]) - self.assertEqual(atom0.label, atom.label) - self.assertEqual(atom0.lone_pairs, atom.lone_pairs) + assert atom0.charge == [c - 1 for c in atom.charge] + assert atom0.label == atom.label + assert atom0.lone_pairs == atom.lone_pairs except ActionError: - self.assertEqual(len(atomtype.decrement_charge), 0) + assert len(atomtype.decrement_charge) == 0 # test when radicals unspecified group = Group().from_adjacency_list(""" @@ -458,34 +456,6 @@ def test_apply_action_lose_pair(self): except ActionError: assert len(atomtype.decrement_lone_pair) == 0 - def test_apply_action_gain_charge(self): - """ - Test the GroupBond.apply_action() method for a GAIN_RADICAL action. - """ - action = ['GAIN_CHARGE', '*1', 1] - for order0 in self.orderList: - bond0 = GroupBond(None, None, order=order0) - bond = bond0.copy() - try: - bond.apply_action(action) - pytest.fail(reason='GroupBond.apply_action() unexpectedly processed a GAIN_CHARGE action.') - except ActionError: - pass - - def test_apply_action_lose_charge(self): - """ - Test the GroupBond.apply_action() method for a LOSE_CHARGE action. - """ - action = ['LOSE_CHARGE', '*1', 1] - for order0 in self.orderList: - bond0 = GroupBond(None, None, order=order0) - bond = bond0.copy() - try: - bond.apply_action(action) - pytest.fail(reason='GroupBond.apply_action() unexpectedly processed a LOSE_CHARGE action.') - except ActionError: - pass - def test_equivalent(self): """ Test the GroupAtom.equivalent() method. @@ -671,14 +641,14 @@ def test_is_electron(self): Test the GroupAtom.is_electron() method. """ electron = GroupAtom(atomtype=[ATOMTYPES['e']]) - self.assertTrue(electron.is_electron()) + assert electron.is_electron() def test_is_proton(self): """ Test the GroupAtom.is_proton() method. """ proton = GroupAtom(atomtype=[ATOMTYPES['H+']]) - self.assertTrue(proton.is_proton()) + assert proton.is_proton() class TestGroupBond: """ @@ -707,6 +677,35 @@ def test_get_order_str(self): """ bond = GroupBond(None, None, order=[1, 2, 3, 1.5]) assert bond.get_order_str() == ["S", "D", "T", "B"] + + + def test_apply_action_gain_charge(self): + """ + Test the GroupBond.apply_action() method for a GAIN_RADICAL action. + """ + action = ['GAIN_CHARGE', '*1', 1] + for order0 in self.orderList: + bond0 = GroupBond(None, None, order=order0) + bond = bond0.copy() + try: + bond.apply_action(action) + pytest.fail(reason='GroupBond.apply_action() unexpectedly processed a GAIN_CHARGE action.') + except ActionError: + pass + + def test_apply_action_lose_charge(self): + """ + Test the GroupBond.apply_action() method for a LOSE_CHARGE action. + """ + action = ['LOSE_CHARGE', '*1', 1] + for order0 in self.orderList: + bond0 = GroupBond(None, None, order=order0) + bond = bond0.copy() + try: + bond.apply_action(action) + pytest.fail(reason='GroupBond.apply_action() unexpectedly processed a LOSE_CHARGE action.') + except ActionError: + pass def test_set_order_str(self): """ @@ -1005,17 +1004,17 @@ def test_is_electron(self): """ Test the Group.is_electron() method. """ - self.assertFalse(self.group.is_electron()) + assert not self.group.is_electron() electron = Group().from_adjacency_list("""1 *1 e u1 p0 c-1""") - self.assertTrue(electron.is_electron()) + assert electron.is_electron() def test_is_proton(self): """ Test the Group.is_proton() method. """ - self.assertFalse(self.group.is_proton()) + assert not self.group.is_proton() proton = Group().from_adjacency_list("""1 *1 H+ u0 p0 c+1""") - self.assertTrue(proton.is_proton()) + assert proton.is_proton() def test_get_labeled_atom(self): """ From 6634774a908af0aa2e8252ae542d0f53b20cff2a Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 8 Jan 2023 22:52:38 -0800 Subject: [PATCH 082/109] enable fitting of TS solute rules --- rmgpy/data/kinetics/family.py | 70 +++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index d4c55aa011..4d74543b79 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4557,11 +4557,75 @@ def _make_rule(rr): for i, rxn in enumerate(rxns): rxn.rank = ranks[i] rxns = np.array(rxns) - rs = np.array([r for r in rxns if type(r.kinetics) != KineticsModel]) # KineticsModel is the base class with no data. + rs = np.array([r for r in rxns if type(r.kinetics) != KineticsModel]) n = len(rs) data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rs])) - - if n == 0: + if n > 0: + if isinstance(rs[0].kinetics, Arrhenius): + arr = ArrheniusBM + else: + arr = ArrheniusChargeTransferBM + if n > 1: + kin = arr().fit_to_reactions(rs, recipe=recipe) + if n == 1 or kin.E0.value_si < 0.0: + kin = average_kinetics([r.kinetics for r in rs]) + #kin.comment = "Only one reaction or Arrhenius BM fit bad. Instead averaged from {} reactions.".format(n) + if n == 1: + kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + else: + dlnks = np.array([ + np.log( + average_kinetics([r.kinetics for r in rs[list(set(range(len(rs))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + else: + if n == 1: + kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + else: + if isinstance(rs[0].kinetics, Arrhenius): + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + else: + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + + #site solute parameters + site_datas = [get_site_solute_data(rxn) for rxn in rxns] + site_datas = [sdata for sdata in site_datas if sdata is not None] + if len(site_datas) > 0: + site_data = SoluteTSData() + for sdata in site_datas: + site_data += sdata + site_data = site_data * (1.0/len(site_datas)) + kin.solute = site_data + return kin + else: return None if isinstance(rs[0].kinetics, Arrhenius): From cadb3342ca0f59157c2f46713bb4dc879ddf1541 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Fri, 8 Dec 2023 15:16:13 -0800 Subject: [PATCH 083/109] allow inclusion of refractive index in Solvent object --- rmgpy/data/solvation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index 276f338d1b..b95f708f02 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -362,7 +362,7 @@ class SolventData(object): def __init__(self, s_h=None, b_h=None, e_h=None, l_h=None, a_h=None, c_h=None, s_g=None, b_g=None, e_g=None, l_g=None, a_g=None, c_g=None, A=None, B=None, - C=None, D=None, E=None, alpha=None, beta=None, eps=None, n=None, name_in_coolprop=None): + C=None, D=None, E=None, alpha=None, beta=None, eps=None, name_in_coolprop=None, n=None): self.s_h = s_h self.b_h = b_h self.e_h = e_h From a52274a329da7ef95bc9cbfc8914198781f82ba0 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Fri, 8 Dec 2023 15:17:37 -0800 Subject: [PATCH 084/109] change TS solvation handling to new system --- rmgpy/data/kinetics/family.py | 11 ++++++----- rmgpy/data/kinetics/library.py | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 4d74543b79..a139935e53 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -220,7 +220,7 @@ def apply_solvent_correction(self, solvent): from rmgpy.data.rmg import get_db solvation_database = get_db('solvation') solvent_data = solvation_database.get_solvent_data(solvent) - site_data = self.kinetics.solute + site_data = to_soluteTSdata(self.kinetics.solute) #compute x from gas phase GR = 0.0 @@ -246,14 +246,15 @@ def apply_solvent_correction(self, solvent): dHR = 0.0 dSR = 0.0 - for spc in rxn.reactants: - spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) - site_data += spc_solute_data*(1.0-x) + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) + spc_soluteTS_data = to_soluteTSdata(spc_solute_data) + site_data += spc_soluteTS_data*(1.0-x) spc_correction = solvation_database.get_solvation_correction(spc_solute_data, solvent_data) dHR += spc_correction.enthalpy dSR += spc_correction.entropy - for spc in rxn.products: + for spc in self.products: spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) site_data += spc_solute_data*x diff --git a/rmgpy/data/kinetics/library.py b/rmgpy/data/kinetics/library.py index 014e799309..bde5958b83 100644 --- a/rmgpy/data/kinetics/library.py +++ b/rmgpy/data/kinetics/library.py @@ -43,11 +43,12 @@ from rmgpy.data.kinetics.common import save_entry from rmgpy.data.kinetics.family import TemplateReaction from rmgpy.kinetics import Arrhenius, ThirdBody, Lindemann, Troe, \ - PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, Chebyshev + PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, Chebyshev, KineticsModel from rmgpy.kinetics.surface import StickingCoefficient from rmgpy.molecule import Molecule from rmgpy.reaction import Reaction from rmgpy.species import Species +from rmgpy.data.solvation import to_soluteTSdata import rmgpy.constants as constants ################################################################################ From e6bd49811a9001c5249c6184ea9128063fd64dc8 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:02:53 -0800 Subject: [PATCH 085/109] added Marcus kinetics --- rmgpy/data/kinetics/database.py | 3 +- rmgpy/data/kinetics/family.py | 23 ++++- rmgpy/data/kinetics/rules.py | 2 +- rmgpy/kinetics/__init__.py | 2 +- rmgpy/kinetics/arrhenius.pxd | 19 ++++ rmgpy/kinetics/arrhenius.pyx | 177 +++++++++++++++++++++++++++++++- rmgpy/rmg/reactors.py | 7 +- 7 files changed, 226 insertions(+), 7 deletions(-) diff --git a/rmgpy/data/kinetics/database.py b/rmgpy/data/kinetics/database.py index 5c87cd8b61..6650a532cc 100644 --- a/rmgpy/data/kinetics/database.py +++ b/rmgpy/data/kinetics/database.py @@ -46,7 +46,7 @@ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, \ Chebyshev, KineticsData, StickingCoefficient, \ StickingCoefficientBEP, SurfaceArrhenius, SurfaceArrheniusBEP, \ - ArrheniusBM, SurfaceChargeTransfer, KineticsModel + ArrheniusBM, SurfaceChargeTransfer, KineticsModel, Marcus from rmgpy.molecule import Molecule, Group from rmgpy.reaction import Reaction, same_species_lists from rmgpy.species import Species @@ -88,6 +88,7 @@ def __init__(self): 'SoluteTSData': SoluteTSData, 'SoluteTSDiffData': SoluteTSDiffData, 'KineticsModel': KineticsModel, + 'Marcus': Marcus, } self.global_context = {} diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index a139935e53..e5678bb70c 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -36,6 +36,7 @@ import multiprocessing as mp import os.path import random +import math import re import warnings from collections import OrderedDict @@ -56,7 +57,7 @@ ForbiddenStructureException, UndeterminableKineticsError from rmgpy.kinetics import Arrhenius, SurfaceArrhenius, SurfaceArrheniusBEP, StickingCoefficient, \ StickingCoefficientBEP, ArrheniusBM, SurfaceChargeTransfer, ArrheniusChargeTransfer, \ - ArrheniusChargeTransferBM, KineticsModel + ArrheniusChargeTransferBM, KineticsModel, Marcus from rmgpy.kinetics.uncertainties import RateUncertainty, rank_accuracy_map from rmgpy.molecule import Bond, GroupBond, Group, Molecule from rmgpy.molecule.atomtype import ATOMTYPES @@ -3547,7 +3548,25 @@ def make_bm_rules_from_template_rxn_map(self, template_rxn_map, nprocs=1, Tref=1 kinetics_list = kinetics_list[revinds] # fix order for i, kinetics in enumerate(kinetics_list): - if kinetics is not None: + if isinstance(kinetics, Marcus): + entry = entries[i] + st = "Marcus rule fitted to {0} training reactions at node {1}".format(len(rxnlists[i][0]), entry.label) + new_entry = Entry( + index=index, + label=entry.label, + item=self.forward_template, + data=kinetics, + rank=11, + reference=None, + short_desc=st, + long_desc=st, + ) + new_entry.data.comment = st + + self.rules.entries[entry.label].append(new_entry) + + index += 1 + elif kinetics is not None: entry = entries[i] std = kinetics.uncertainty.get_expected_log_uncertainty() / 0.398 # expected uncertainty is std * 0.398 st = "BM rule fitted to {0} training reactions at node {1}".format(len(rxnlists[i][0]), entry.label) diff --git a/rmgpy/data/kinetics/rules.py b/rmgpy/data/kinetics/rules.py index a87cfb8a14..d20a4a6ce4 100644 --- a/rmgpy/data/kinetics/rules.py +++ b/rmgpy/data/kinetics/rules.py @@ -45,7 +45,7 @@ from rmgpy.data.kinetics.common import save_entry from rmgpy.exceptions import KineticsError, DatabaseError from rmgpy.kinetics import ArrheniusEP, Arrhenius, StickingCoefficientBEP, SurfaceArrheniusBEP, \ - SurfaceChargeTransfer, SurfaceChargeTransferBEP + SurfaceChargeTransfer, SurfaceChargeTransferBEP, Marcus from rmgpy.quantity import Quantity, ScalarQuantity from rmgpy.reaction import Reaction diff --git a/rmgpy/kinetics/__init__.py b/rmgpy/kinetics/__init__.py index 2f3d3626d1..b3f8eec12e 100644 --- a/rmgpy/kinetics/__init__.py +++ b/rmgpy/kinetics/__init__.py @@ -30,7 +30,7 @@ from rmgpy.kinetics.model import KineticsModel, PDepKineticsModel, TunnelingModel, \ get_rate_coefficient_units_from_reaction_order, get_reaction_order_from_rate_coefficient_units from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, \ - ArrheniusBM, ArrheniusChargeTransfer, ArrheniusChargeTransferBM + ArrheniusBM, ArrheniusChargeTransfer, ArrheniusChargeTransferBM, Marcus from rmgpy.kinetics.chebyshev import Chebyshev from rmgpy.kinetics.falloff import ThirdBody, Lindemann, Troe from rmgpy.kinetics.kineticsdata import KineticsData, PDepKineticsData diff --git a/rmgpy/kinetics/arrhenius.pxd b/rmgpy/kinetics/arrhenius.pxd index 08f3b95010..2d39ac48d4 100644 --- a/rmgpy/kinetics/arrhenius.pxd +++ b/rmgpy/kinetics/arrhenius.pxd @@ -180,3 +180,22 @@ cdef class ArrheniusChargeTransferBM(KineticsModel): cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 cpdef change_rate(self, double factor) + +cdef class Marcus(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ArrayQuantity _lmbd_i_coefs + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _beta + cdef public ScalarQuantity _wr + cdef public ScalarQuantity _wp + cdef public ScalarQuantity _lmbd_o + + cpdef double get_lmbd_i(self, double T) + + cpdef double get_gibbs_activation_energy(self, double T, double dGrxn) except -1 + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef change_rate(self, double factor) \ No newline at end of file diff --git a/rmgpy/kinetics/arrhenius.pyx b/rmgpy/kinetics/arrhenius.pyx index fc8c69937a..f4b69179ed 100644 --- a/rmgpy/kinetics/arrhenius.pyx +++ b/rmgpy/kinetics/arrhenius.pyx @@ -37,6 +37,7 @@ import rmgpy.quantity as quantity from rmgpy.exceptions import KineticsError from rmgpy.kinetics.uncertainties import rank_accuracy_map from rmgpy.molecule.molecule import Bond +from rmgpy.kinetics.model import KineticsModel, PDepKineticsModel import logging # Prior to numpy 1.14, `numpy.linalg.lstsq` does not accept None as a value @@ -1690,6 +1691,180 @@ cdef class ArrheniusChargeTransferBM(KineticsModel): """ raise NotImplementedError('set_cantera_kinetics() is not implemented for ArrheniusEP class kinetics.') +cdef class Marcus(KineticsModel): + """ + A kinetics model based on the (modified) Arrhenius equation, using the + Evans-Polanyi equation to determine the activation energy. The attributes + are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `n` The temperature exponent + `lmbd_i_coefs` Coefficients for inner sphere reorganization energy + `V0` The reference potential + `beta` Transmission decay coefficient + `wr` Work to bring reactants together + `wp` Work to bring products together + `lmbd_o` Outer sphere reorganization energy (solvent) + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, lmbd_i_coefs=np.array([0.0,0.0,0.0,0.0]), beta=(1.2e-10,"1/m"), + wr=(0,"J/mol"), wp=(0,"J/mol"), lmbd_o=(0,"J/mol"), Tmin=None, Tmax=None, + Pmin=None, Pmax=None, solute=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, solute=solute, uncertainty=uncertainty, + comment=comment) + + self.A = A + self.n = n + self.lmbd_i_coefs = lmbd_i_coefs + self.beta = beta + self.wr = wr + self.wp = wp + self.lmbd_o = lmbd_o + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Marcus object. + """ + string = 'Marcus(A={0!r}, n={1!r}, lmbd_i_coefs={2!r}, beta={3!r}, wr={4!r}, wp={5!r}, lmbd_o={6!r}'.format( + self.A, self.n, self.lmbd_i_coefs, self.beta, self.wr, self.wp, self.lmbd_o) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.solute: string += ', solute={0!r}'.format(self.solute) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a Marcus object. + """ + return (Marcus, (self.A, self.n, self.lmbd_i_coefs, self.beta, self.wr, self.wp, self.lmbd_o, + self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.solute, self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.RateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property lmbd_i_coefs: + """Temperature polynomial coefficients for inner sphere reogranization energy""" + def __get__(self): + return self._lmbd_i_coefs + def __set__(self, value): + self._lmbd_i_coefs = quantity.Dimensionless(value) + + property beta: + """transmission coefficient""" + def __get__(self): + return self._beta + def __set__(self, value): + self._beta = quantity.UnitType('m^-1')(value) + + property lmbd_o: + """outer sphere reorganization energy""" + def __get__(self): + return self._lmbd_o + def __set__(self, value): + self._lmbd_o = quantity.Energy(value) + + property wr: + """outer sphere reorganization energy""" + def __get__(self): + return self._wr + def __set__(self, value): + self._wr = quantity.Energy(value) + + property wp: + """outer sphere reorganization energy""" + def __get__(self): + return self._wp + def __set__(self, value): + self._wp = quantity.Energy(value) + + cpdef double get_rate_coefficient(self, double T, double dGrxn=0.0) except -1: + """ + Return the rate coefficient in the appropriate combination of m^3, + mol, and s at temperature `T` in K and enthalpy of reaction `dHrxn` + in J/mol. + """ + cdef double A, n, dG + dG = self.get_gibbs_activation_energy(T, dGrxn) + A = self._A.value_si + n = self._n.value_si + return A * T ** n * exp(-dG / (constants.R * T)) + + cpdef double get_lmbd_i(self, double T): + """ + Return lmbd_i in J/mol + """ + return self.lmbd_i_coefs.value_si[0]+self.lmbd_i_coefs.value_si[1]*T+self.lmbd_i_coefs.value_si[2]*T**2+self.lmbd_i_coefs.value_si[3]*T**3 + + cpdef double get_gibbs_activation_energy(self, double T, double dGrxn) except -1: + """ + Return the activation energy in J/mol corresponding to the given + enthalpy of reaction `dHrxn` in J/mol. + """ + cdef double lmbd_i + lmbd_i = self.get_lmbd_i(T) + return (lmbd_i+self.lmbd_o.value_si)/4.0*(1.0+dGrxn/(lmbd_i+self.lmbd_o.value_si))**2 + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, Marcus): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.lmbd_i_coefs.equals(other_kinetics.lmbd_i_coefs) or not self.lmbd_o.equals(other_kinetics.lmbd_o) + or not self.beta.equals(other_kinetics.beta) + or not self.electrons.equals(other_kinetics.electrons)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + + def set_cantera_kinetics(self, ct_reaction, species_list): + """ + Sets a cantera ElementaryReaction() object with the modified Arrhenius object + converted to an Arrhenius form. + """ + raise NotImplementedError('set_cantera_kinetics() is not implemented for Marcus class kinetics.') + def get_w0(actions, rxn): """ calculates the w0 for Blower Masel kinetics by calculating wf (total bond energy of bonds formed) @@ -1760,4 +1935,4 @@ def get_w0(actions, rxn): return (wf + wb) / 2.0 def get_w0s(actions, rxns): - return [get_w0(actions, rxn) for rxn in rxns] + return [get_w0(actions, rxn) for rxn in rxns] \ No newline at end of file diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index be79ebc9bb..2c09d138b5 100644 --- a/rmgpy/rmg/reactors.py +++ b/rmgpy/rmg/reactors.py @@ -34,6 +34,7 @@ import sys import logging import itertools +import rmgpy.constants as constants if __debug__: try: @@ -62,7 +63,7 @@ from rmgpy.thermo.nasa import NASAPolynomial, NASA from rmgpy.thermo.wilhoit import Wilhoit from rmgpy.thermo.thermodata import ThermoData -from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, ArrheniusBM, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, ArrheniusChargeTransfer +from rmgpy.kinetics.arrhenius import Arrhenius, ArrheniusEP, ArrheniusBM, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, ArrheniusChargeTransfer, Marcus from rmgpy.kinetics.kineticsdata import KineticsData from rmgpy.kinetics.falloff import Troe, ThirdBody, Lindemann from rmgpy.kinetics.chebyshev import Chebyshev @@ -611,6 +612,10 @@ def to_rms(obj, species_names=None, rms_species_list=None, rmg_species=None): Ea = obj._Ea.value_si q = obj._alpha.value_si*obj._electrons.value_si return rms.Arrheniusq(A, n, Ea, q, rms.EmptyRateUncertainty()) + elif isinstance(obj, Marcus): + A = obj._A.value_si + n = obj._n.value_si + return rms.Marcus(A,n,obj._lmbd_i_coefs.value_si,obj._lmbd_o.value_si, obj._wr.value_si, obj._wp.value_si, obj._beta.value_si, rms.EmptyRateUncertainty()) elif isinstance(obj, PDepArrhenius): Ps = obj._pressures.value_si arrs = [to_rms(arr) for arr in obj.arrhenius] From 21c960fb82d007b0246ac8fad534453611245fbe Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:07:41 -0800 Subject: [PATCH 086/109] allow direct specification of viscosity and electrode distance for the RMS reactor --- rmgpy/rmg/input.py | 10 +++++++++- rmgpy/rmg/reactors.py | 15 ++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/rmgpy/rmg/input.py b/rmgpy/rmg/input.py index de1b3e237a..117c19644c 100644 --- a/rmgpy/rmg/input.py +++ b/rmgpy/rmg/input.py @@ -634,6 +634,8 @@ def liquid_cat_reactor(temperature, initialConcentrations, initialSurfaceCoverages, surfaceVolumeRatio, + distance=None, + viscosity=None, surfPotential=None, liqPotential=None, terminationConversion=None, @@ -711,13 +713,19 @@ def liquid_cat_reactor(temperature, initialCondSurf[key] = item*rmg.surface_site_density.value_si*A initialCondSurf["T"] = T initialCondSurf["A"] = A + initialCondSurf["d"] = 0.0 if surfPotential: initialCondSurf["Phi"] = Quantity(surfPotential).value_si if liqPotential: initialCondLiq["Phi"] = Quantity(liqPotential).value_si + if distance: + initialCondLiq["d"] = Quantity(distance).value_si + if viscosity: + initialCondLiq["mu"] = Quantity(distance).value_si system = ConstantTLiquidSurfaceReactor(rmg.reaction_model.core.phase_system, rmg.reaction_model.edge.phase_system, - {"liquid":initialCondLiq,"surface":initialCondSurf},termination,constantSpecies) + {"liquid":initialCondLiq,"surface":initialCondSurf}, + termination,constantSpecies) system.T = Quantity(T) system.Trange = None system.sensitive_species = [] diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index 2c09d138b5..ac1a207e13 100644 --- a/rmgpy/rmg/reactors.py +++ b/rmgpy/rmg/reactors.py @@ -490,14 +490,19 @@ def generate_reactor(self, phase_system): liq = phase_system.phases["Default"] surf = phase_system.phases["Surface"] interface = list(phase_system.interfaces.values())[0] - liq = rms.IdealDiluteSolution(liq.species, liq.reactions, liq.solvent, name="liquid") + if "mu" in self.initial_conditions["liquid"].keys(): + solv = rms.Solvent("solvent",rms.ConstantViscosity(self.initial_conditions["liquid"]["mu"])) + liq_initial_cond = self.initial_conditions["liquid"].copy() + del liq_initial_cond["mu"] + else: + solv = liq.solvent + liq_initial_cond = self.initial_conditions["liquid"] + liq = rms.IdealDiluteSolution(liq.species, liq.reactions, solv, name="liquid",diffusionlimited=True) surf = rms.IdealSurface(surf.species, surf.reactions, surf.site_density, name="surface") liq_constant_species = [cspc for cspc in self.const_spc_names if cspc in [spc.name for spc in liq.species]] cat_constant_species = [cspc for cspc in self.const_spc_names if cspc in [spc.name for spc in surf.species]] - domainliq, y0liq, pliq = rms.ConstantTVDomain(phase=liq, initialconds=self.initial_conditions["liquid"], constantspecies=liq_constant_species) - domaincat, y0cat, pcat = rms.ConstantTAPhiDomain( - phase=surf, initialconds=self.initial_conditions["surface"], constantspecies=cat_constant_species - ) + domainliq,y0liq,pliq = rms.ConstantTVDomain(phase=liq,initialconds=liq_initial_cond,constantspecies=liq_constant_species) + domaincat,y0cat,pcat = rms.ConstantTAPhiDomain(phase=surf,initialconds=self.initial_conditions["surface"],constantspecies=cat_constant_species) if interface.reactions == []: inter, pinter = rms.ReactiveInternalInterfaceConstantTPhi( domainliq, From d339918805166b4c526d46a6e99f25ceca2192b6 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:09:01 -0800 Subject: [PATCH 087/109] handle electronchange when constructing RMS objects specify electronchange in RMS yaml construction --- rmgpy/rmg/reactors.py | 18 +++--------------- rmgpy/yml.py | 1 + 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index ac1a207e13..04445e040d 100644 --- a/rmgpy/rmg/reactors.py +++ b/rmgpy/rmg/reactors.py @@ -784,21 +784,9 @@ def to_rms(obj, species_names=None, rms_species_list=None, rmg_species=None): reactants = [rms_species_list[i] for i in reactantinds] products = [rms_species_list[i] for i in productinds] kinetics = to_rms(obj.kinetics, species_names=species_names, rms_species_list=rms_species_list, rmg_species=rmg_species) - radchange = sum([spc.molecule[0].multiplicity - 1 for spc in obj.products]) - sum([spc.molecule[0].multiplicity - 1 for spc in obj.reactants]) - electronchange = 0 # for now - return rms.ElementaryReaction( - index=obj.index, - reactants=reactants, - reactantinds=reactantinds, - products=products, - productinds=productinds, - kinetics=kinetics, - electronchange=electronchange, - radicalchange=radchange, - reversible=obj.reversible, - pairs=[], - comment=obj.kinetics.comment, - ) + radchange = sum([spc.molecule[0].multiplicity-1 for spc in obj.products]) - sum([spc.molecule[0].multiplicity-1 for spc in obj.reactants]) + electronchange = -sum([spc.molecule[0].get_net_charge() for spc in obj.products]) + sum([spc.molecule[0].get_net_charge() for spc in obj.reactants]) + return rms.ElementaryReaction(index=obj.index, reactants=reactants, reactantinds=reactantinds, products=products, productinds=productinds, kinetics=kinetics, electronchange=electronchange, radicalchange=radchange, reversible=obj.reversible, pairs=[], comment=obj.kinetics.comment) elif isinstance(obj, SolventData): return rms.Solvent("solvent", rms.RiedelViscosity(float(obj.A), float(obj.B), float(obj.C), float(obj.D), float(obj.E))) elif isinstance(obj, TerminationTime): diff --git a/rmgpy/yml.py b/rmgpy/yml.py index 76db07386a..fd785d1452 100644 --- a/rmgpy/yml.py +++ b/rmgpy/yml.py @@ -141,6 +141,7 @@ def obj_to_dict(obj, spcs, names=None, label="solvent"): result_dict["type"] = "ElementaryReaction" result_dict["radicalchange"] = sum([get_radicals(x) for x in obj.products]) - \ sum([get_radicals(x) for x in obj.reactants]) + result_dict["electronchange"] = -sum([spc.molecule[0].get_net_charge() for spc in obj.products]) + sum([spc.molecule[0].get_net_charge() for spc in obj.reactants]) result_dict["comment"] = obj.kinetics.comment elif isinstance(obj, Arrhenius): obj.change_t0(1.0) From f4fa9bf2cd46694ac43742da1c0f2fd747e0a27f Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:10:07 -0800 Subject: [PATCH 088/109] handle Marcus in RMS yaml constructionn --- rmgpy/yml.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/rmgpy/yml.py b/rmgpy/yml.py index fd785d1452..8b6e9f771f 100644 --- a/rmgpy/yml.py +++ b/rmgpy/yml.py @@ -34,13 +34,14 @@ import os import yaml +import logging from rmgpy.chemkin import load_chemkin_file from rmgpy.species import Species from rmgpy.reaction import Reaction from rmgpy.thermo.nasa import NASAPolynomial, NASA from rmgpy.thermo.wilhoit import Wilhoit -from rmgpy.kinetics.arrhenius import Arrhenius, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, ArrheniusChargeTransfer +from rmgpy.kinetics.arrhenius import Arrhenius, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, ArrheniusChargeTransfer, Marcus from rmgpy.kinetics.falloff import Troe, ThirdBody, Lindemann from rmgpy.kinetics.chebyshev import Chebyshev from rmgpy.data.solvation import SolventData @@ -164,6 +165,15 @@ def obj_to_dict(obj, spcs, names=None, label="solvent"): result_dict["Ea"] = obj.Ea.value_si result_dict["n"] = obj.n.value_si result_dict["q"] = obj._alpha.value_si*obj._electrons.value_si + elif isinstance(obj, Marcus): + result_dict["type"] = "Marcus" + result_dict["A"] = obj.A.value_si + result_dict["n"] = obj.n.value_si + result_dict["lmbd_i_coefs"] = obj.lmbd_i_coefs.value_si.tolist() + result_dict["lmbd_o"] = obj.lmbd_o.value_si + result_dict["wr"] = obj.wr.value_si + result_dict["wp"] = obj.wp.value_si + result_dict["beta"] = obj.beta.value_si elif isinstance(obj, StickingCoefficient): obj.change_t0(1.0) result_dict["type"] = "StickingCoefficient" From 54ea6e399de672622c35175702148574e164f5e6 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:11:00 -0800 Subject: [PATCH 089/109] handle Marcus kinetics in fix_barrier_height --- rmgpy/reaction.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index f8836ba020..af96ed9e1c 100644 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -53,7 +53,7 @@ from rmgpy.exceptions import ReactionError, KineticsError from rmgpy.kinetics import KineticsData, ArrheniusBM, ArrheniusEP, ThirdBody, Lindemann, Troe, Chebyshev, \ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, get_rate_coefficient_units_from_reaction_order, \ - SurfaceArrheniusBEP, StickingCoefficientBEP, ArrheniusChargeTransfer, ArrheniusChargeTransferBM + SurfaceArrheniusBEP, StickingCoefficientBEP, ArrheniusChargeTransfer, ArrheniusChargeTransferBM, Marcus from rmgpy.kinetics.arrhenius import Arrhenius # Separate because we cimport from rmgpy.kinetics.arrhenius from rmgpy.kinetics.surface import SurfaceArrhenius, StickingCoefficient, SurfaceChargeTransfer, SurfaceChargeTransferBEP # Separate because we cimport from rmgpy.kinetics.surface from rmgpy.kinetics.diffusionLimited import diffusion_limiter @@ -996,8 +996,11 @@ def fix_barrier_height(self, force_positive=False, solvent="", apply_solvation_c if self.kinetics is None: raise KineticsError("Cannot fix barrier height for reactions with no kinetics attribute") - - if isinstance(self.kinetics, SurfaceChargeTransferBEP): + + if isinstance(self.kinetics, Marcus): + if apply_solvation_correction and solvent: + self.apply_solvent_correction(solvent) + elif isinstance(self.kinetics, SurfaceChargeTransferBEP): Ea = self.kinetics.E0.value_si # temporarily using Ea to store the intrinsic barrier height E0 V0 = self.kinetics.V0.value_si deltaG = self._get_free_energy_of_charge_transfer_reaction(298,V0) From 2fcea6d816e57c2463c2568599abc20fa8381ba8 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:11:50 -0800 Subject: [PATCH 090/109] handle solvation for Marcus kinetics --- rmgpy/data/kinetics/family.py | 27 +++++++++++++++++++++++++++ rmgpy/data/kinetics/library.py | 24 +++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index e5678bb70c..4cffbc1f85 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -221,6 +221,30 @@ def apply_solvent_correction(self, solvent): from rmgpy.data.rmg import get_db solvation_database = get_db('solvation') solvent_data = solvation_database.get_solvent_data(solvent) + + + if isinstance(self.kinetics, Marcus): + solvent_struct = solvation_database.get_solvent_structure(solvent)[0] + solv_solute_data = solvation_database.get_solute_data(solvent_struct.copy(deep=True)) + Rsolv = math.pow((75 * solv_solute_data.V / constants.pi / constants.Na), + (1.0 / 3.0)) / 100 + Rtot = 0.0 + Ner = 0 + Nep = 0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) + spc_solute_data.set_mcgowan_volume(spc) + R = math.pow((75 * spc_solute_data.V / constants.pi / constants.Na), + (1.0 / 3.0)) / 100 + Rtot += R + Ner += spc.get_net_charge() + for spc in self.products: + Nep += spc.get_net_charge() + + Rtot += Rsolv #radius of reactants plus first solvation shell + self.lmbd_o = constants.Na*(constants.e*(Nep-Ner))**2/(8.0*constants.pi*constants.epsilon_0*Rtot)*(1.0/solvent_data.n**2 - 1.0/solvent_data.eps) + return + site_data = to_soluteTSdata(self.kinetics.solute) #compute x from gas phase @@ -238,7 +262,10 @@ def apply_solvent_correction(self, solvent): except Exception: logging.error("Problem with product {!r} in reaction {!s}".format(reactant, self)) raise + + GTS = self.kinetics.Ea.value_si + GR + #x = abs(GTS - GR) / (abs(GP - GTS) + abs(GR - GTS)) dGrxn = GP-GR if dGrxn > 0: x = 1.0 diff --git a/rmgpy/data/kinetics/library.py b/rmgpy/data/kinetics/library.py index bde5958b83..4298084a8c 100644 --- a/rmgpy/data/kinetics/library.py +++ b/rmgpy/data/kinetics/library.py @@ -43,7 +43,7 @@ from rmgpy.data.kinetics.common import save_entry from rmgpy.data.kinetics.family import TemplateReaction from rmgpy.kinetics import Arrhenius, ThirdBody, Lindemann, Troe, \ - PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, Chebyshev, KineticsModel + PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, Chebyshev, KineticsModel, Marcus from rmgpy.kinetics.surface import StickingCoefficient from rmgpy.molecule import Molecule from rmgpy.reaction import Reaction @@ -227,6 +227,28 @@ def apply_solvent_correction(self, solvent): from rmgpy.data.rmg import get_db solvation_database = get_db('solvation') solvent_data = solvation_database.get_solvent_data(solvent) + + if isinstance(self.kinetics, Marcus): + solvent_struct = solvation_database.get_solvent_structure(solvent) + solv_solute_data = solvation_database.get_solute_data(solvent_struct.copy(deep=True)) + Rsolv = math.pow((75 * solv_solute_data.V / constants.pi / constants.Na) * (1.0 / 3.0)) / 100 + Rtot = 0.0 + Ner = 0 + Nep = 0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) + spc_solute_data.set_mcgowan_volume(spc) + R = math.pow((75 * spc_solute_data.V / constants.pi / constants.Na), + (1.0 / 3.0)) / 100 + Rtot += R + Ner += spc.get_net_charge() + for spc in self.products: + Nep += spc.get_net_charge() + + Rtot += Rsolv #radius of reactants plus first solvation shell + self.lmbd_o = constants.Na*(constants.e*(Nep-Ner))**2/(8.0*constants.pi*constants.epsilon_0*Rtot)*(1.0/solvent_data.n**2 - 1.0/solvent_data.eps) + return + solute_data = to_soluteTSdata(self.kinetics.solute,reactants=self.reactants) dGTS,dHTS = solute_data.calculate_corrections(solvent_data) dSTS = (dHTS - dGTS)/298.0 From 7051f5783a19aba40d77509db1fed82f3703a0c5 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:16:28 -0800 Subject: [PATCH 091/109] handle kinetics averaging for Marcus --- rmgpy/data/kinetics/family.py | 84 +++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 4cffbc1f85..9498e73690 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4773,36 +4773,9 @@ def average_kinetics(kinetics_list): Hence we average n, Ea, arithmetically, but we average log A (geometric average) """ - logA = 0.0 - n = 0.0 - Ea = 0.0 - alpha = 0.5 - electrons = None - if isinstance(kinetics_list[0], (SurfaceChargeTransfer, ArrheniusChargeTransfer)): - if electrons is None: - electrons = kinetics_list[0].electrons.value_si - if not all(np.abs(k.V0.value_si) < 0.0001 for k in kinetics_list): - raise ValueError(f"Trying to average charge transfer rates with non-zero V0 values: {[k.V0.value_si for k in kinetics_list]}") - if not all(np.abs(k.alpha.value_si - 0.5) < 0.001 for k in kinetics_list): - raise ValueError(f"Trying to average charge transfer rates with alpha values not equal to 0.5: {[k.alpha for k in kinetics_list]}") - V0 = 0.0 - count = 0 - for kinetics in kinetics_list: - count += 1 - logA += np.log10(kinetics.A.value_si) - n += kinetics.n.value_si - Ea += kinetics.Ea.value_si - - logA /= count - n /= count - alpha /= count - Ea /= count - - ## The above could be replaced with something like: - # logA, n, Ea = np.mean([[np.log10(k.A.value_si), - # k.n.value_si, - # k.Ea.value_si] for k in kinetics_list], axis=1) - + if type(kinetics_list[0]) not in [Arrhenius,SurfaceChargeTransfer,ArrheniusChargeTransfer,Marcus]: + raise Exception('Invalid kinetics type {0!r} for {1!r}.'.format(type(kinetics), self)) + Aunits = kinetics_list[0].A.units if Aunits in {'cm^3/(mol*s)', 'cm^3/(molecule*s)', 'm^3/(molecule*s)'}: Aunits = 'm^3/(mol*s)' @@ -4821,12 +4794,55 @@ def average_kinetics(kinetics_list): # surface: sticking coefficient pass else: - raise ValueError(f'Invalid units {Aunits} for averaging kinetics.') + raise Exception('Invalid units {0} for averaging kinetics.'.format(Aunits)) + + logA = 0.0 + n = 0.0 + Ea = 0.0 + alpha = 0.5 + lmbd_i_coefs = np.zeros(4) + beta = 0.0 + wr = 0.0 + wp = 0.0 + electrons = None + if isinstance(kinetics_list[0], SurfaceChargeTransfer) or isinstance(kinetics_list[0], ArrheniusChargeTransfer): + if electrons is None: + electrons = kinetics_list[0].electrons.value_si + assert all(np.abs(k.V0.value_si) < 0.0001 for k in kinetics_list), [k.V0.value_si for k in kinetics_list] + assert all(np.abs(k.alpha.value_si - 0.5) < 0.001 for k in kinetics_list), [k.alpha for k in kinetics_list] + V0 = 0.0 + count = 0 + for kinetics in kinetics_list: + count += 1 + logA += np.log10(kinetics.A.value_si) + n += kinetics.n.value_si + if hasattr(kinetics,"Ea"): + Ea += kinetics.Ea.value_si + if hasattr(kinetics,"lmbd_i_coefs"): + lmbd_i_coefs += kinetics.lmbd_i_coefs.value_si + beta += kinetics.beta.value_si + wr += kinetics.wr.value_si + wp += kinetics.wp.value_si - if type(kinetics) not in {Arrhenius, SurfaceChargeTransfer, ArrheniusChargeTransfer}: - raise TypeError(f'Invalid kinetics type {type(kinetics)!r} for {self!r}.') + logA /= count + n /= count + Ea /= count + lmbd_i_coefs /= count + beta /= count + wr /= count + wp /= count - if isinstance(kinetics, SurfaceChargeTransfer): + if isinstance(kinetics, Marcus): + averaged_kinetics = Marcus( + A=(10 ** logA, Aunits), + n=n, + lmbd_i_coefs=lmbd_i_coefs, + beta=(beta,"1/m"), + wr=(wr * 0.001, "kJ/mol"), + wp=(wp * 0.001, "kJ/mol"), + comment="Averaged from {} reactions.".format(len(kinetics_list)), + ) + elif isinstance(kinetics, SurfaceChargeTransfer): averaged_kinetics = SurfaceChargeTransfer( A=(10 ** logA, Aunits), n=n, From 1d1cd1c13e3c194f1ae6ccf7456625fda5a72dc7 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:17:07 -0800 Subject: [PATCH 092/109] handle Marcus kinetics within tree generation and rule generation --- rmgpy/data/kinetics/family.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 9498e73690..8ba433e34a 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -4580,8 +4580,12 @@ def get_objective_function(kinetics1, kinetics2, obj=information_gain, T=1000.0) Error using mean: Err_1 + Err_2 Split: abs(N1-N2) """ - ks1 = np.array([np.log(k.get_rate_coefficient(T)) for k in kinetics1]) - ks2 = np.array([np.log(k.get_rate_coefficient(T)) for k in kinetics2]) + if not isinstance(kinetics1[0], Marcus): + ks1 = np.array([np.log(k.get_rate_coefficient(T)) for k in kinetics1]) + ks2 = np.array([np.log(k.get_rate_coefficient(T)) for k in kinetics2]) + else: + ks1 = np.array([k.get_lmbd_i(T) for k in kinetics1]) + ks2 = np.array([k.get_lmbd_i(T) for k in kinetics2]) N1 = len(ks1) return obj(ks1, ks2), N1 == 0 @@ -4606,6 +4610,9 @@ def _make_rule(rr): rxns = np.array(rxns) rs = np.array([r for r in rxns if type(r.kinetics) != KineticsModel]) n = len(rs) + if n > 0 and isinstance(rs[0].kinetics, Marcus): + kin = average_kinetics([r.kinetics for r in rs]) + return kin data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rs])) if n > 0: if isinstance(rs[0].kinetics, Arrhenius): From f34cb2dfca5bead50b5ebb4a684eaa8cd806900e Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:40:57 -0800 Subject: [PATCH 093/109] add ethylene carbonate and acetonitrile SEI examples --- examples/rmg/SEI_pure_ACN/input.py | 181 ++++++++++++++++++ examples/rmg/SEI_pure_EC/input.py | 291 +++++++++++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 examples/rmg/SEI_pure_ACN/input.py create mode 100644 examples/rmg/SEI_pure_EC/input.py diff --git a/examples/rmg/SEI_pure_ACN/input.py b/examples/rmg/SEI_pure_ACN/input.py new file mode 100644 index 0000000000..2a4136c59c --- /dev/null +++ b/examples/rmg/SEI_pure_ACN/input.py @@ -0,0 +1,181 @@ +# Data sources +database( + thermoLibraries=['LithiumSurface','electrocatLiThermo','primaryThermoLibrary', 'LithiumPrimaryThermo', 'LithiumAdditionalThermo', 'thermo_DFT_CCSDTF12_BAC','DFT_QCI_thermo'], # 'surfaceThermoPt' is the default. Thermo data is derived using bindingEnergies for other metals + reactionLibraries = ['LithiumPrimaryKinetics',"LithiumSurface"], # when Ni is used change the library to Surface/Deutschmann_Ni + seedMechanisms = [], + kineticsDepositories = ['training'], + kineticsFamilies = ['surface', + '1+2_Cycloaddition', + 'Surface_Carbonate_Deposition', + 'Surface_Carbonate_F_CO_Decomposition', + 'Surface_Carbonate_2F_Decomposition', + 'Surface_Carbonate_CO_Decomposition', + '1,2_Elimination_LiR', + '1,2_Intra_Elimination_LiR', + 'Li_Addition_MultipleBond', + 'Li_NO_Substitution', + 'Li_NO_Ring_Opening', + 'Li_Abstraction', + 'R_Addition_MultipleBond_Disprop', + 'Cation_R_Recombination', + 'Cation_Addition_MultipleBond', + '1,2-Birad_to_alkene', + '1,2_Insertion_CO', + '1,2_Insertion_carbene', + '1,2_shiftS', + '1,3_Insertion_CO2', + '1,3_Insertion_ROR', + '1,3_Insertion_RSR', + '1,4_Cyclic_birad_scission', + '1,4_Linear_birad_scission', + '2+2_cycloaddition', + 'Birad_recombination', + 'CO_Disproportionation', + 'Birad_R_Recombination', + 'Cyclic_Ether_Formation', + 'Cyclic_Thioether_Formation', + 'Diels_alder_addition', + 'Diels_alder_addition_Aromatic', + #'Disproportionation', + 'HO2_Elimination_from_PeroxyRadical', + 'H_Abstraction', + 'Intra_Retro_Diels_alder_bicyclic', + 'Intra_Disproportionation', + 'Intra_R_Add_Endocyclic', + 'Intra_R_Add_Exocyclic', + 'R_Addition_COm', + 'R_Addition_MultipleBond', + 'R_Recombination', + 'intra_H_migration', + 'intra_NO2_ONO_conversion', + 'intra_OH_migration', + 'intra_substitutionCS_cyclization', + 'intra_substitutionCS_isomerization', + 'intra_substitutionS_cyclization', + 'intra_substitutionS_isomerization', + #'ketoenol', + 'Singlet_Carbene_Intra_Disproportionation', + 'Singlet_Val6_to_triplet', + 'Intra_5_membered_conjugated_C=C_C=C_addition', + 'Intra_Diels_alder_monocyclic', + 'Concerted_Intra_Diels_alder_monocyclic_1,2_shiftH', + 'Intra_2+2_cycloaddition_Cd', + 'Intra_ene_reaction', + 'Cyclopentadiene_scission', + '6_membered_central_C-C_shift', + 'Intra_R_Add_Exo_scission', + '1,2_shiftC', + '1,2_NH3_elimination', + '1,3_NH3_elimination', + 'Retroene',], + kineticsEstimator = 'rate rules', + adsorptionGroups='adsorptionLi', +) + +catalystProperties( + metal = 'Li110', +) + +# List of species +species( + label="Lip", + reactive=True, + structure=SMILES("[Li+]"), +) + +species( + label='ACN', + reactive=True, + structure=SMILES("CC#N"), +) + +species( + label='vacantX', + reactive=True, + structure=adjacencyList("1 X u0"), +) + +liquidSurfaceReactor( + temperature=(298.15,'K'), + distance=(10.0e-10,"m"), + viscosity=(5e7,"Pa*s"), + liqPotential=(0.3,'V'), + surfPotential=(0.0,'V'), + initialConcentrations={ + "ACN": (0.019146,'mol/cm^3'), + "Lip": (15.0,'mol/m^3'), + }, + initialSurfaceCoverages={ + "vacantX": 1.0, + }, + surfaceVolumeRatio=(1.0e-5, 'm^-1'), + terminationTime=(1e3,'sec'), + constantSpecies=["ACN","Lip"], +) + +liquidSurfaceReactor( + temperature=(298.15,'K'), + distance=(0.0,"m"), + liqPotential=(0.0,'V'), + surfPotential=(0.0,'V'), + initialConcentrations={ + "ACN": (0.019146,'mol/cm^3'), + "Lip": (15.0,'mol/m^3'), + }, + initialSurfaceCoverages={ + "vacantX": 1.0, + }, + surfaceVolumeRatio=(1.0e5, 'm^-1'), + terminationTime=(1e3,'sec'), + constantSpecies=["ACN","Lip"], +) + +solvation( + solvent='acetonitrile' +) + +simulator( + atol=1e-16, + rtol=1e-6, +) + +model( + toleranceKeepInEdge=1E-20, + toleranceMoveToCore=0.1, + toleranceRadMoveToCore=0.1, + toleranceInterruptSimulation=1e10, + maximumEdgeSpecies=100000, + filterReactions=False, + maxNumObjsPerIter=1, + terminateAtMaxObjects=True, + toleranceBranchReactionToCore=0.001, + branchingIndex=0.5, + branchingRatioMax=1.0, +) + +options( + units='si', + saveEdgeSpecies=False, +) + +forbidden( + label='vacancies', + structure=adjacencyListGroup(""" +1 Xv u0 p0 c0 +"""), +) + +forbidden( + label='Li2', + structure=adjacencyList(""" +1 Li u0 p0 c0 {2,S} +2 Li u0 p0 c0 {1,S}"""), +) + +generatedSpeciesConstraints( + allowed=['input species','reaction libraries'], + maximumSurfaceSites=1, + maximumCarbonAtoms=7, + maximumOxygenAtoms=4, + maximumRadicalElectrons=1, +) diff --git a/examples/rmg/SEI_pure_EC/input.py b/examples/rmg/SEI_pure_EC/input.py new file mode 100644 index 0000000000..2912c7f2ba --- /dev/null +++ b/examples/rmg/SEI_pure_EC/input.py @@ -0,0 +1,291 @@ +# Data sources +database( + thermoLibraries=['electrocatLiThermo','primaryThermoLibrary', 'LithiumPrimaryThermo', 'LithiumAdditionalThermo', 'thermo_DFT_CCSDTF12_BAC','DFT_QCI_thermo'], # 'surfaceThermoPt' is the default. Thermo data is derived using bindingEnergies for other metals + reactionLibraries = ['LithiumPrimaryKinetics','LithiumAnalogyKinetics'], # when Ni is used change the library to Surface/Deutschmann_Ni + seedMechanisms = [], + kineticsDepositories = ['training'], + kineticsFamilies = ['surface','electrochem', + '1+2_Cycloaddition', + '1,2-Birad_to_alkene', + '1,2_Insertion_CO', + '1,2_Insertion_carbene', + '1,2_shiftS', + '1,3_Insertion_CO2', + '1,3_Insertion_ROR', + '1,3_Insertion_RSR', + '1,4_Cyclic_birad_scission', + '1,4_Linear_birad_scission', + '2+2_cycloaddition', + 'Birad_recombination', + 'CO_Disproportionation', + 'Birad_R_Recombination', + 'Cyclic_Ether_Formation', + 'Cyclic_Thioether_Formation', + 'Diels_alder_addition', + 'Diels_alder_addition_Aromatic', + #'Disproportionation', + 'HO2_Elimination_from_PeroxyRadical', + 'H_Abstraction', + 'Intra_Retro_Diels_alder_bicyclic', + 'Intra_Disproportionation', + 'Intra_R_Add_Endocyclic', + 'Intra_R_Add_Exocyclic', + 'R_Addition_COm', + 'R_Addition_MultipleBond', + 'R_Recombination', + 'intra_H_migration', + 'intra_NO2_ONO_conversion', + 'intra_OH_migration', + 'intra_substitutionCS_cyclization', + 'intra_substitutionCS_isomerization', + 'intra_substitutionS_cyclization', + 'intra_substitutionS_isomerization', + #'ketoenol', + 'Singlet_Carbene_Intra_Disproportionation', + 'Singlet_Val6_to_triplet', + 'Intra_5_membered_conjugated_C=C_C=C_addition', + 'Intra_Diels_alder_monocyclic', + 'Concerted_Intra_Diels_alder_monocyclic_1,2_shiftH', + 'Intra_2+2_cycloaddition_Cd', + 'Intra_ene_reaction', + 'Cyclopentadiene_scission', + '6_membered_central_C-C_shift', + 'Intra_R_Add_Exo_scission', + '1,2_shiftC', + '1,2_NH3_elimination', + '1,3_NH3_elimination', + 'Retroene',], + kineticsEstimator = 'rate rules', + adsorptionGroups='adsorptionLi', +) + +catalystProperties( + metal = 'Li110' +) + +# List of species +species( + label="Lip", + reactive=True, + structure=SMILES("[Li+]"), +) + +species( + label='ethylene-carbonate', + reactive=True, + structure=SMILES("C1COC(=O)O1"), +) + +species( + label='vacantX', + reactive=True, + structure=adjacencyList("1 X u0"), +) + +species( + label="Li", + reactive=True, + structure=SMILES("[Li]"), +) + +species( + label='[Li]O[C]1OCCO1', + reactive=True, + structure=SMILES("[Li]O[C]1OCCO1"), +) + +species( + label='[Li]OC(=O)OC[CH2]', + reactive=True, + structure=SMILES("[Li]OC(=O)OC[CH2]"), +) + +#species( +# label='[Li]OC(=O)O[Li]', +# reactive=True, +# structure=SMILES("[Li]OC(=O)O[Li]"), +# ) + +#species( +# label='[Li]OC(=O)OCCOC(=O)OC[CH2]', +# reactive=True, +# structure=SMILES("[Li]OC(=O)OCCOC(=O)OC[CH2]"), +# ) + +#species( +# label='[Li]OC(=O)OCCOC(=O)O[Li]', +# reactive=True, +# structure=SMILES("[Li]OC(=O)OCCOC(=O)O[Li]"), +# ) + +#species( +# label='[Li]OC(=O)OCCCCOC(=O)O[Li]', +# reactive=True, +# structure=SMILES("[Li]OC(=O)OCCCCOC(=O)O[Li]"),) + +#species( +# label='[Li]OCCOC(=O)CCOC(=O)O[Li]', +# reactive=True, +# structure=SMILES("[Li]OCCOC(=O)CCOC(=O)O[Li]"), +# ) + +#species( +# label='[Li]OCCOC(=O)OC(=O)O[Li]', +# reactive=True, +# structure=SMILES("[Li]OCCOC(=O)OC(=O)O[Li]"), +#) + +#species( +# label='C2H4', +# reactive=True, +# structure=SMILES("C=C"), +#) + +#species( +# label='O=[C]OCCO[Li]', +# reactive=True, +# structure=SMILES("O=[C]OCCO[Li]"), +#) + +#species( +# label='CO2', +# reactive=True, +# structure=SMILES("O=C=O"), +#) + +#species( +# label='[Li]OC[CH2]', +# reactive=True, +# structure=SMILES("[Li]OC[CH2]"), +#) + +#species( +# label='O1CCO[C]1OC2(O[Li])OCCO2', +# reactive=True, +# structure=SMILES("O1CCO[C]1OC2(O[Li])OCCO2"), +#) + +#species( +# label='O1CCOC1(O[Li])OC(=O)OC[CH2]', +# reactive=True, +# structure=SMILES("O1CCOC1(O[Li])OC(=O)OC[CH2]"), +#) + +#species( +# label='O1CCOC1(O[Li])OC(=O)OCCOC(=O)O[Li]', +# reactive=True, +# structure=SMILES("O1CCOC1(O[Li])OC(=O)OCCOC(=O)O[Li]"), +#) + +species( + label='CO3X2', + reactive=True, + structure=adjacencyList("""1 O u0 p2 {2,D} +2 C u0 p0 {1,D} {3,S} {4,S} +3 O u0 p2 {2,S} {5,S} +4 O u0 p2 {2,S} {6,S} +5 X u0 p0 c0 {3,S} +6 X u0 p0 c0 {4,S} +"""), +) + + +#species( +# label="CO", +# reactive=True, +# structure=SMILES("[C-]#[O+]"), +#) + +#species( +# label='[Li]OC(=O)OCCX', +# reactive=True, +# structure=adjacencyList("""1 O u0 p2 c0 {2,S} {7,S} +# 2 C u0 p0 c0 {1,S} {3,D} {4,S} +# 3 O u0 p2 c0 {2,D} +# 4 O u0 p2 c0 {2,S} {5,S} +# 5 C u0 p0 c0 {4,S} {6,S} {8,S} {9,S} +# 6 C u0 p0 c0 {5,S} {10,S} {11,S} {12,S} +# 7 Li u0 p0 c0 {1,S} +# 8 H u0 p0 c0 {5,S} +# 9 H u0 p0 c0 {5,S} +# 10 H u0 p0 c0 {6,S} +# 11 H u0 p0 c0 {6,S} +# 12 X u0 p0 c0 {6,S} +# """), +#) +#species( +# label='O=C(X)OCCO[Li]', +# reactive=True, +# structure=adjacencyList("""1 O u0 p2 c0 {2,D} +# 2 C u0 p0 c0 {1,D} {3,S} {12,S} +# 3 O u0 p2 c0 {2,S} {4,S} +# 4 C u0 p0 c0 {3,S} {5,S} {7,S} {8,S} +# 5 C u0 p0 c0 {4,S} {6,S} {9,S} {10,S} +# 6 O u0 p2 c0 {5,S} {11,S} +# 7 H u0 p0 c0 {4,S} +# 8 H u0 p0 c0 {4,S} +# 9 H u0 p0 c0 {5,S} +# 10 H u0 p0 c0 {5,S} +# 11 Li u0 p0 c0 {6,S} +# 12 X u0 p0 c0 {2,S} +# """), +#) + +liquidSurfaceReactor( + temperature=(298.15,'K'), + liqPotential=(-1.0,'V'), + surfPotential=(0.0,'V'), + initialConcentrations={ + "ethylene-carbonate": (7.585e-3*2.0,'mol/cm^3'), + "Lip": (15.0,'mol/m^3'), + }, + initialSurfaceCoverages={ + "vacantX": 1.0, + }, + surfaceVolumeRatio=(1.0e5, 'm^-1'), + terminationTime=(1.0e3,'sec'), + constantSpecies=["ethylene-carbonate","Lip"], +) + +solvation( + solvent='ethylene carbonate' +) + +simulator( + atol=1e-16, + rtol=1e-6, +) + +model( + toleranceKeepInEdge=1E-20, + toleranceMoveToCore=0.000001, + toleranceRadMoveToCore=0.00000000001, + toleranceInterruptSimulation=1e10, + maximumEdgeSpecies=100000, + filterReactions=False, + maxNumObjsPerIter=1, + terminateAtMaxObjects=True, + toleranceBranchReactionToCore=0.000001, + branchingIndex=0.3, + branchingRatioMax=1.0, +) + +options( + units='si', + saveEdgeSpecies=False, +) + +forbidden( + label='vacancies', + structure=adjacencyListGroup(""" +1 Xv u0 p0 c0 +"""), +) + +generatedSpeciesConstraints( + allowed=['input species','reaction libraries'], + maximumSurfaceSites=1, + maximumCarbonAtoms=8, + maximumOxygenAtoms=8, + maximumRadicalElectrons=1, +) From f9c1988c1e38bfec284c3507a126bd8516fca8f4 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 9 Mar 2024 18:06:10 -0800 Subject: [PATCH 094/109] fix bug with species initialization --- rmgpy/rmg/main.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rmgpy/rmg/main.py b/rmgpy/rmg/main.py index 14d4591c8f..fb4ae33ef8 100644 --- a/rmgpy/rmg/main.py +++ b/rmgpy/rmg/main.py @@ -555,9 +555,6 @@ def initialize(self, **kwargs): # Load databases self.load_database() - for spec in self.initial_species: - self.reaction_model.add_species_to_edge(spec) - for reaction_system in self.reaction_systems: if isinstance(reaction_system, Reactor): reaction_system.finish_termination_criteria() @@ -639,6 +636,14 @@ def initialize(self, **kwargs): # Initialize reaction model + for spec in self.initial_species: + if spec.reactive: + submit(spec, self.solvent) + if vapor_liquid_mass_transfer.enabled: + spec.get_liquid_volumetric_mass_transfer_coefficient_data() + spec.get_henry_law_constant_data() + self.reaction_model.add_species_to_edge(spec) + # Seed mechanisms: add species and reactions from seed mechanism # DON'T generate any more reactions for the seed species at this time for seed_mechanism in self.seed_mechanisms: From 5286c9501bfe3c78b897698422d803cf5612444d Mon Sep 17 00:00:00 2001 From: ssun30 Date: Fri, 15 Mar 2024 15:27:09 -0400 Subject: [PATCH 095/109] Fixed a typo in test lose charge --- test/rmgpy/molecule/groupTest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rmgpy/molecule/groupTest.py b/test/rmgpy/molecule/groupTest.py index 8aa98a1826..1a6c3d2879 100644 --- a/test/rmgpy/molecule/groupTest.py +++ b/test/rmgpy/molecule/groupTest.py @@ -338,7 +338,7 @@ def test_apply_action_lose_charge(self): assert a in atom.atomtype,"LOSE_CHARGE on {0} gave {1} not {2}".format(atomtype, atom.atomtype, atomtype.decrement_charge) # self.assertEqual(atom0.radical_electrons, [r - 1 for r in atom.radical_electrons]) - assert atom0.charge == [c - 1 for c in atom.charge] + assert atom0.charge == [c + 1 for c in atom.charge] assert atom0.label == atom.label assert atom0.lone_pairs == atom.lone_pairs except ActionError: From ce4f3e11cfa5a69dc20eba08c316371ad978e4a5 Mon Sep 17 00:00:00 2001 From: ssun30 Date: Fri, 12 Apr 2024 14:31:37 -0400 Subject: [PATCH 096/109] Add set_reference_potential for SurfaceChargeTransfer in to_rms --- rmgpy/rmg/reactors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index 04445e040d..26b78e4008 100644 --- a/rmgpy/rmg/reactors.py +++ b/rmgpy/rmg/reactors.py @@ -783,6 +783,8 @@ def to_rms(obj, species_names=None, rms_species_list=None, rmg_species=None): productinds = [species_names.index(spc.label) for spc in obj.products] reactants = [rms_species_list[i] for i in reactantinds] products = [rms_species_list[i] for i in productinds] + if isinstance(obj.kinetics, SurfaceChargeTransfer): + obj.set_reference_potential(obj.kinetics._T0.value_si) kinetics = to_rms(obj.kinetics, species_names=species_names, rms_species_list=rms_species_list, rmg_species=rmg_species) radchange = sum([spc.molecule[0].multiplicity-1 for spc in obj.products]) - sum([spc.molecule[0].multiplicity-1 for spc in obj.reactants]) electronchange = -sum([spc.molecule[0].get_net_charge() for spc in obj.products]) + sum([spc.molecule[0].get_net_charge() for spc in obj.reactants]) From e1169c90fb57c5fc5ea81a0ee9c07029f3ec2cdb Mon Sep 17 00:00:00 2001 From: ssun30 Date: Wed, 6 Nov 2024 11:21:40 -0500 Subject: [PATCH 097/109] Set reference potential now always sets to 300 K --- rmgpy/rmg/reactors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index 26b78e4008..3b843febbf 100644 --- a/rmgpy/rmg/reactors.py +++ b/rmgpy/rmg/reactors.py @@ -784,7 +784,7 @@ def to_rms(obj, species_names=None, rms_species_list=None, rmg_species=None): reactants = [rms_species_list[i] for i in reactantinds] products = [rms_species_list[i] for i in productinds] if isinstance(obj.kinetics, SurfaceChargeTransfer): - obj.set_reference_potential(obj.kinetics._T0.value_si) + obj.set_reference_potential(300) kinetics = to_rms(obj.kinetics, species_names=species_names, rms_species_list=rms_species_list, rmg_species=rmg_species) radchange = sum([spc.molecule[0].multiplicity-1 for spc in obj.products]) - sum([spc.molecule[0].multiplicity-1 for spc in obj.reactants]) electronchange = -sum([spc.molecule[0].get_net_charge() for spc in obj.products]) + sum([spc.molecule[0].get_net_charge() for spc in obj.reactants]) From 69f4ee15991104cf3f6ae3fb320c0ffa58a83c19 Mon Sep 17 00:00:00 2001 From: ssun30 Date: Wed, 6 Nov 2024 11:23:38 -0500 Subject: [PATCH 098/109] Fixed A-factor correction for SurfaceChargeTransfer Now it's done in RMS Added solvent correction for SurfaceChargeTransfer --- rmgpy/reaction.py | 4 ++-- rmgpy/rmg/reactors.py | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index af96ed9e1c..90471acc19 100644 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -996,7 +996,7 @@ def fix_barrier_height(self, force_positive=False, solvent="", apply_solvation_c if self.kinetics is None: raise KineticsError("Cannot fix barrier height for reactions with no kinetics attribute") - + if isinstance(self.kinetics, Marcus): if apply_solvation_correction and solvent: self.apply_solvent_correction(solvent) @@ -1039,7 +1039,7 @@ def fix_barrier_height(self, force_positive=False, solvent="", apply_solvation_c logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol.".format( self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) self.kinetics.Ea.value_si = Ea - if isinstance(self.kinetics, (Arrhenius, StickingCoefficient, ArrheniusChargeTransfer)): # SurfaceArrhenius is a subclass of Arrhenius + if isinstance(self.kinetics, (Arrhenius, StickingCoefficient, ArrheniusChargeTransfer, SurfaceChargeTransfer)): # SurfaceArrhenius is a subclass of Arrhenius if apply_solvation_correction and solvent and self.kinetics.solute: self.apply_solvent_correction(solvent) Ea = self.kinetics.Ea.value_si diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index 3b843febbf..9386fa2a20 100644 --- a/rmgpy/rmg/reactors.py +++ b/rmgpy/rmg/reactors.py @@ -601,22 +601,20 @@ def to_rms(obj, species_names=None, rms_species_list=None, rmg_species=None): A = obj._A.value_si if obj._T0.value_si != 1.0: A /= ((obj._T0.value_si) ** obj._n.value_si) - if obj._V0.value_si != 0.0: - A *= np.exp(obj._alpha.value_si*obj._electrons.value_si*constants.F*obj.V0.value_si) n = obj._n.value_si Ea = obj._Ea.value_si q = obj._alpha.value_si*obj._electrons.value_si - return rms.Arrheniusq(A, n, Ea, q, rms.EmptyRateUncertainty()) + V0 = obj._V0.value_si + return rms.Arrheniusq(A, n, Ea, q, V0, rms.EmptyRateUncertainty()) elif isinstance(obj, SurfaceChargeTransfer): A = obj._A.value_si if obj._T0.value_si != 1.0: A /= ((obj._T0.value_si) ** obj._n.value_si) - if obj._V0.value_si != 0.0: - A *= np.exp(obj._alpha.value_si*obj._electrons.value_si*constants.F*obj.V0.value_si) n = obj._n.value_si Ea = obj._Ea.value_si q = obj._alpha.value_si*obj._electrons.value_si - return rms.Arrheniusq(A, n, Ea, q, rms.EmptyRateUncertainty()) + V0 = obj._V0.value_si + return rms.Arrheniusq(A, n, Ea, q, V0, rms.EmptyRateUncertainty()) elif isinstance(obj, Marcus): A = obj._A.value_si n = obj._n.value_si @@ -725,7 +723,7 @@ def to_rms(obj, species_names=None, rms_species_list=None, rmg_species=None): else: atomnums[atm.element.symbol] = 1 bondnum = len(mol.get_all_edges()) - + if not obj.molecule[0].contains_surface_site(): rad = rms.getspeciesradius(atomnums, bondnum) diff = rms.StokesDiffusivity(rad) From 465210122b8e566049da9cd0b0fd71eb125c1849 Mon Sep 17 00:00:00 2001 From: ssun30 Date: Fri, 7 Jun 2024 10:31:33 -0400 Subject: [PATCH 099/109] Added CO2RR example --- examples/rmg/CO2RR/input.py | 317 ++++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 examples/rmg/CO2RR/input.py diff --git a/examples/rmg/CO2RR/input.py b/examples/rmg/CO2RR/input.py new file mode 100644 index 0000000000..182f878588 --- /dev/null +++ b/examples/rmg/CO2RR/input.py @@ -0,0 +1,317 @@ +# Data sources +database( + thermoLibraries=['surfaceThermoPt111', 'primaryThermoLibrary', 'thermo_DFT_CCSDTF12_BAC','DFT_QCI_thermo', 'electrocatThermo', + # 'CO2RR_Adsorbates_Ag111' + ], + reactionLibraries = [('Surface/CPOX_Pt/Deutschmann2006_adjusted', False)], + seedMechanisms = [], + kineticsDepositories = ['training'], + kineticsFamilies = ['electrochem', + # 'surface', + 'Surface_Abstraction', + 'Surface_Abstraction_vdW', + 'Surface_Abstraction_Single_vdW', + 'Surface_Abstraction_Beta_double_vdW', + 'Surface_Adsorption_Dissociative', + 'Surface_Adsorption_Dissociative_Double', + 'Surface_Adsorption_vdW', + 'Surface_Dissociation', + 'Surface_Dissociation_Double_vdW', + 'Surface_Dissociation_vdW', + 'Surface_EleyRideal_Addition_Multiple_Bond', + 'Surface_Migration', + ], + kineticsEstimator = 'rate rules', + +) + +catalystProperties( + metal = 'Ag111' +) + +# List of species +species( + label='CO2', + reactive=True, + structure=adjacencyList( + """ +1 O u0 p2 c0 {2,D} +2 C u0 p0 c0 {1,D} {3,D} +3 O u0 p2 c0 {2,D} +"""), +) + + +species( + label='proton', + reactive=True, + structure=adjacencyList( + """ +1 H u0 p0 c+1 +"""), +) + +species( + label='vacantX', + reactive=True, + structure=adjacencyList("1 X u0"), +) + +species( + label='H', + reactive=True, + structure=adjacencyList( + """ +1 H u1 p0 c0 +"""), +) + +species( + label='CO2X', + reactive=True, + structure=adjacencyList(""" +1 O u0 p2 c0 {3,D} +2 O u0 p2 c0 {3,D} +3 C u0 p0 c0 {1,D} {2,D} +4 X u0 p0 c0 +"""), +) + +species( + label='CHO2X', + reactive=True, + structure=adjacencyList(""" +1 O u0 p2 c0 {3,S} {5,S} +2 O u0 p2 c0 {3,D} +3 C u0 p0 c0 {1,S} {2,D} {4,S} +4 H u0 p0 c0 {3,S} +5 X u0 p0 c0 {1,S} +"""), +) + +species( + label='CO2HX', + reactive=True, + structure=adjacencyList(""" +1 O u0 p2 c0 {2,S} {4,S} +2 C u0 p0 c0 {1,S} {3,D} {5,S} +3 O u0 p2 c0 {2,D} +4 H u0 p0 c0 {1,S} +5 X u0 p0 c0 {2,S} + +"""), +) + +species( + label='OCX', + reactive=True, + structure=adjacencyList(""" +1 O u0 p2 c0 {2,D} +2 C u0 p0 c0 {1,D} {3,D} +3 X u0 p0 c0 {2,D} +"""), +) + +species( + label='OX', + reactive=True, + structure=adjacencyList(""" +1 O u0 p2 c0 {2,D} +2 X u0 p0 c0 {1,D} +"""), +) + +species( + label='CH2O2X', + reactive=True, + structure=adjacencyList(""" +1 O u0 p2 c0 {3,S} {5,S} +2 O u0 p2 c0 {3,D} +3 C u0 p0 c0 {1,S} {2,D} {4,S} +4 H u0 p0 c0 {3,S} +5 H u0 p0 c0 {1,S} +6 X u0 p0 c0 +"""), +) + +species( + label='CHOX', + reactive=True, + structure=adjacencyList(""" +1 O u0 p2 c0 {2,D} +2 C u0 p0 c0 {1,D} {3,S} {4,S} +3 H u0 p0 c0 {2,S} +4 X u0 p0 c0 {2,S} +"""), +) + +species( + label='CH2OX', + reactive=True, + structure=adjacencyList(""" +1 O u0 p2 c0 {2,D} +2 C u0 p0 c0 {1,D} {3,S} {4,S} +3 H u0 p0 c0 {2,S} +4 H u0 p0 c0 {2,S} +5 X u0 p0 c0 +"""), +) + + +forbidden( + label='CO2-bidentate', + structure=adjacencyList( + """ + 1 O u0 p2 c0 {2,D} + 2 C u0 p0 c0 {1,D} {3,S} {4,S} + 3 X u0 p0 c0 {2,S} + 4 O u0 p2 c0 {2,S} {5,S} + 5 X u0 p0 c0 {4,S} + """ + ) +) + +liquidSurfaceReactor( + temperature=(300,'K'), + liqPotential=(0,'V'), + surfPotential=(-2.0,'V'), + initialConcentrations={ + "CO2": (1e-3,'mol/cm^3'), + "proton": (1e-4,'mol/m^3'), + }, + initialSurfaceCoverages={ + # "HX": 0.5, + # # "CXO2": 0.0, + "CHO2X": 0.1, + "CO2HX": 0.1, + "vacantX": 0.1, + "CO2X": 0.4, + 'OX': 0.1, + 'OCX': 0.1, + 'CH2O2X': 0.05, + 'CHOX': 0.04, + 'CH2OX': 0.01 + }, + surfaceVolumeRatio=(1.0e5, 'm^-1'), + terminationTime=(1.0e3,'sec'), + # terminationConversion={'CO2': 0.90}, + # constantSpecies=["proton"], + ) + +liquidSurfaceReactor( + temperature=(300,'K'), + liqPotential=(0,'V'), + surfPotential=(-1.5,'V'), + initialConcentrations={ + "CO2": (1e-3,'mol/cm^3'), + "proton": (1e-4,'mol/m^3'), + }, + initialSurfaceCoverages={ + # "HX": 0.5, + # # "CXO2": 0.0, + "CHO2X": 0.1, + "CO2HX": 0.1, + "vacantX": 0.1, + "CO2X": 0.4, + 'OX': 0.1, + 'OCX': 0.1, + 'CH2O2X': 0.05, + 'CHOX': 0.04, + 'CH2OX': 0.01 + }, + surfaceVolumeRatio=(1.0e5, 'm^-1'), + terminationTime=(1.0e3,'sec'), + # terminationConversion={'CO2': 0.90}, + # constantSpecies=["proton"], + ) + +liquidSurfaceReactor( + temperature=(300,'K'), + liqPotential=(0,'V'), + surfPotential=(-1.0,'V'), + initialConcentrations={ + "CO2": (1e-3,'mol/cm^3'), + "proton": (1e-4,'mol/m^3'), + }, + initialSurfaceCoverages={ + # "HX": 0.5, + # # "CXO2": 0.0, + "CHO2X": 0.1, + "CO2HX": 0.1, + "vacantX": 0.1, + "CO2X": 0.4, + 'OX': 0.1, + 'OCX': 0.1, + 'CH2O2X': 0.05, + 'CHOX': 0.04, + 'CH2OX': 0.01 + }, + surfaceVolumeRatio=(1.0e5, 'm^-1'), + terminationTime=(1.0e3,'sec'), + # terminationConversion={'CO2': 0.90}, + # constantSpecies=["proton"], + ) + +# liquidSurfaceReactor( +# temperature=(300,'K'), +# liqPotential=(0,'V'), +# surfPotential=(-0.5,'V'), +# initialConcentrations={ +# "CO2": (1e-3,'mol/cm^3'), +# "proton": (1e-4,'mol/m^3'), +# }, +# initialSurfaceCoverages={ +# # "HX": 0.5, +# # # "CXO2": 0.0, +# "CHO2X": 0.1, +# "CO2HX": 0.1, +# "vacantX": 0.1, +# "CO2X": 0.4, +# 'OX': 0.1, +# 'OCX': 0.1, +# 'CH2O2X': 0.05, +# 'CHOX': 0.04, +# 'CH2OX': 0.01 +# }, +# surfaceVolumeRatio=(1.0e5, 'm^-1'), +# terminationTime=(1.0e3,'sec'), +# # terminationConversion={'CO2': 0.90}, +# # constantSpecies=["proton"], +# ) + +solvation( + solvent='water' +) + +simulator( + atol=1e-16, + rtol=1e-8, +) + +model( + toleranceKeepInEdge=1E-16, + toleranceMoveToCore=1E-3, + toleranceRadMoveToCore=1E-6, + toleranceInterruptSimulation=1E6, + filterReactions=False, + maximumEdgeSpecies=5000, + toleranceBranchReactionToCore=1E-6, + branchingIndex=0.5, + branchingRatioMax=1.0, +) + +options( + units='si', + generateOutputHTML=True, + generatePlots=True, + saveEdgeSpecies=True, + saveSimulationProfiles=False, +) + +generatedSpeciesConstraints( + allowed=['input species','reaction libraries'], + maximumSurfaceSites=2, + maximumCarbonAtoms=3, + maximumOxygenAtoms=2, + maximumRadicalElectrons=1, +) From b71fbce50d8d8814ea22bf2fec935d5310cb2cf5 Mon Sep 17 00:00:00 2001 From: Richard West Date: Fri, 3 May 2024 15:11:54 -0400 Subject: [PATCH 100/109] Extra error logging in HBI thermo estimation. --- rmgpy/data/thermo.py | 13 +++++++++---- rmgpy/rmg/model.py | 6 +++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/rmgpy/data/thermo.py b/rmgpy/data/thermo.py index c357b5b230..34f9630dac 100644 --- a/rmgpy/data/thermo.py +++ b/rmgpy/data/thermo.py @@ -2064,8 +2064,12 @@ def estimate_radical_thermo_via_hbi(self, molecule, stable_thermo_estimator): "not {0}".format(thermo_data_sat)) thermo_data_sat = thermo_data_sat[0] else: - thermo_data_sat = stable_thermo_estimator(saturated_struct) - + try: + thermo_data_sat = stable_thermo_estimator(saturated_struct) + except DatabaseError as e: + logging.error(f"Trouble finding thermo data for saturated structure {saturated_struct.to_adjacency_list()}" + f"when trying to evaluate radical {molecule.to_adjacency_list()} via HBI.") + raise e if thermo_data_sat is None: # We couldn't get thermo for the saturated species from libraries, ml, or qm # However, if we were trying group additivity, this could be a problem @@ -2570,9 +2574,10 @@ def _add_group_thermo_data(self, thermo_data, database, molecule, atom): node = node0 while node is not None and node.data is None: node = node.parent - if node is None: + if node is None: raise DatabaseError(f'Unable to determine thermo parameters for atom {atom} in molecule {molecule}: ' - f'no data for node {node0} or any of its ancestors in database {database.label}.') + f'no data for node {node0} or any of its ancestors in database {database.label}.\n' + + molecule.to_adjacency_list()) data = node.data comment = node.label diff --git a/rmgpy/rmg/model.py b/rmgpy/rmg/model.py index 72b95b9bad..ee6795806b 100644 --- a/rmgpy/rmg/model.py +++ b/rmgpy/rmg/model.py @@ -815,7 +815,11 @@ def process_new_reactions(self, new_reactions, new_species, pdep_network=None, g Makes a reaction and decides where to put it: core, edge, or PDepNetwork. """ for rxn in new_reactions: - rxn, is_new = self.make_new_reaction(rxn, generate_thermo=generate_thermo, generate_kinetics=generate_kinetics) + try: + rxn, is_new = self.make_new_reaction(rxn, generate_thermo=generate_thermo, generate_kinetics=generate_kinetics) + except Exception as e: + logging.error(f"Error when making reaction {rxn} from {rxn.family}") + raise e if rxn is None: # Skip this reaction because there was something wrong with it continue From 3d7cea1a8c5b5646367c6ae521c1d19de342694f Mon Sep 17 00:00:00 2001 From: ssun30 Date: Wed, 25 Sep 2024 14:49:44 -0400 Subject: [PATCH 101/109] Ignore PCET vdW families for surface coverage test PCET families have to be skipped since the SurfaceChargeTransfer and SurfaceChargeTransferBEP classes have no attribute "coverage_dependence" --- arkane/encorr/ae.py | 5 +++-- test/database/databaseTest.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/arkane/encorr/ae.py b/arkane/encorr/ae.py index 779de3072b..ecef89ae37 100644 --- a/arkane/encorr/ae.py +++ b/arkane/encorr/ae.py @@ -74,8 +74,9 @@ 'Methyl', 'Ammonia', 'Chloromethane', - 'Lithium Hydride', - 'Lithium Fluoride' + # Lithium species shall be uncommented after we reconcile the difference in AECs and BACs + # 'Lithium Hydride', + # 'Lithium Fluoride' ] diff --git a/test/database/databaseTest.py b/test/database/databaseTest.py index d4285d9f97..3663af56f0 100644 --- a/test/database/databaseTest.py +++ b/test/database/databaseTest.py @@ -132,7 +132,9 @@ def test_kinetics(self): ), "Kinetics surface family {0}: entries have surface attributes?".format(family_name) if family_name not in { "Surface_Proton_Electron_Reduction_Alpha", + "Surface_Proton_Electron_Reduction_Alpha_vdW", "Surface_Proton_Electron_Reduction_Beta", + "Surface_Proton_Electron_Reduction_Beta_vdW", "Surface_Proton_Electron_Reduction_Beta_Dissociation", }: with check: From f2f0cbdd4b4f095223484f431699aea5583c4234 Mon Sep 17 00:00:00 2001 From: ssun30 Date: Wed, 25 Sep 2024 14:30:53 -0400 Subject: [PATCH 102/109] Changed rate coefficient test to use rtol kineticsSurfaceTest used atol for rate coefficient tests previously. The atol was too strict for double precision floating point. All other rate coefficient tests used rtol. --- test/rmgpy/kinetics/kineticsSurfaceTest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/rmgpy/kinetics/kineticsSurfaceTest.py b/test/rmgpy/kinetics/kineticsSurfaceTest.py index d159b27382..5aeaf0b746 100644 --- a/test/rmgpy/kinetics/kineticsSurfaceTest.py +++ b/test/rmgpy/kinetics/kineticsSurfaceTest.py @@ -741,9 +741,11 @@ def test_get_rate_coefficient(self): Ea = Ea_ox - (alpha_ox * electrons_ox * constants.F * (V-V0_ox)) k_oxidation = A_ox * (T / T0_ox) ** n_ox * np.exp(-Ea / (constants.R * T)) Ea = Ea_red - (alpha_red * electrons_red * constants.F * (V-V0_red)) - k_reduction = A_red * (T / T0_red) ** n_red * np.exp(-Ea / (constants.R * T)) - assert round(abs(k_oxidation-self.surfchargerxn_oxidation.get_rate_coefficient(T,V)), 7) == 0 - assert round(abs(k_reduction-self.surfchargerxn_reduction.get_rate_coefficient(T,V)), 7) == 0 + k_reduction = A_red * (T / T0_red) ** n_red * np.exp(-Ea / (constants.R * T)) + kox = self.surfchargerxn_oxidation.get_rate_coefficient(T,V) + kred = self.surfchargerxn_reduction.get_rate_coefficient(T,V) + assert abs(k_oxidation - kox) < 1e-6 * kox + assert abs(k_reduction - kred) < 1e-6 * kred def test_change_v0(self): From 27449c4e6662f128a8dfa35ca80257743c43b117 Mon Sep 17 00:00:00 2001 From: ssun30 Date: Wed, 6 Nov 2024 12:37:34 -0500 Subject: [PATCH 103/109] update_charge skips cutting labels --- rmgpy/molecule/group.py | 2 ++ rmgpy/molecule/molecule.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/rmgpy/molecule/group.py b/rmgpy/molecule/group.py index e445f0ce24..f596e7ab84 100644 --- a/rmgpy/molecule/group.py +++ b/rmgpy/molecule/group.py @@ -1418,6 +1418,8 @@ def update_charge(self): and radical electrons. This method is used for products of specific families with recipes that modify charges. """ for atom in self.atoms: + if isinstance(atom, CuttingLabel): + continue if (len(atom.charge) == 1) and (len(atom.lone_pairs) == 1) and (len(atom.radical_electrons) == 1): # if the charge of the group is not labeled, then no charge update will be # performed. If there multiple charges are assigned, no update either. diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index 26ae067998..0867d87f89 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -1276,7 +1276,8 @@ def sort_atoms(self): def update_charge(self): for atom in self.atoms: - atom.update_charge() + if not isinstance(atom, CuttingLabel): + atom.update_charge() def update(self, log_species=True, raise_atomtype_exception=True, sort_atoms=True): """ From 7ec54b23e73d49bad8019fe775b6c028cf828cef Mon Sep 17 00:00:00 2001 From: ssun30 Date: Wed, 6 Nov 2024 12:52:30 -0500 Subject: [PATCH 104/109] Fixed Fragment test The `Fragment` class is now a subclass of `Molecule`. As such, some if statements for checking atom charges no longer work for Fragments since CuttingLabels don't have defined charges. These if statements were modified such that charge checks for `Fragments` ignore CuttingLabels. --- rmgpy/data/kinetics/family.py | 7 ++++--- rmgpy/molecule/group.py | 1 + rmgpy/molecule/molecule.py | 1 + rmgpy/reaction.py | 18 ++++++++---------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 8ba433e34a..7c0ec5405d 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -1520,9 +1520,10 @@ def apply_recipe(self, reactant_structures, forward=True, unique=True, relabel_a # (families with charged substances), the charge of structures will be updated if isinstance(struct, Molecule): struct.update_charge() - struct.update(sort_atoms=not self.save_order) - elif isinstance(struct, Fragment): - struct.update() + if isinstance(struct, Fragment): + struct.update() + else: + struct.update(sort_atoms=not self.save_order) elif isinstance(struct, Group): is_molecule = False struct.reset_ring_membership() diff --git a/rmgpy/molecule/group.py b/rmgpy/molecule/group.py index f596e7ab84..1dc56cc7f7 100644 --- a/rmgpy/molecule/group.py +++ b/rmgpy/molecule/group.py @@ -44,6 +44,7 @@ from rmgpy.molecule.atomtype import ATOMTYPES, allElements, nonSpecifics, get_features, AtomType from rmgpy.molecule.element import PeriodicSystem from rmgpy.molecule.graph import Vertex, Edge, Graph +from rmgpy.molecule.fragment import CuttingLabel ################################################################################ diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index 0867d87f89..3f647c336a 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -58,6 +58,7 @@ from rmgpy.molecule.graph import Vertex, Edge, Graph, get_vertex_connectivity_value from rmgpy.molecule.kekulize import kekulize from rmgpy.molecule.pathfinder import find_shortest_path +from rmgpy.molecule.fragment import CuttingLabel ################################################################################ diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index 90471acc19..19136c60fa 100644 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -1440,16 +1440,15 @@ def is_balanced(self): if not isinstance(atom, CuttingLabel): reactants_net_charge += atom.charge reactant_elements[atom.element] += 1 - elif isinstance(reactant, Molecule): - molecule = reactant - for atom in molecule.atoms: - reactants_net_charge += atom.charge - reactant_elements[atom.element] += 1 elif isinstance(reactant, Fragment): for atom in reactant.atoms: if not isinstance(atom, CuttingLabel): reactants_net_charge += atom.charge reactant_elements[atom.element] += 1 + elif isinstance(reactant, Molecule): + for atom in reactant.atoms: + reactants_net_charge += atom.charge + reactant_elements[atom.element] += 1 for product in self.products: if isinstance(product, Species): molecule = product.molecule[0] @@ -1457,16 +1456,15 @@ def is_balanced(self): if not isinstance(atom, CuttingLabel): products_net_charge += atom.charge product_elements[atom.element] += 1 - elif isinstance(product, Molecule): - molecule = product - for atom in molecule.atoms: - products_net_charge += atom.charge - product_elements[atom.element] += 1 elif isinstance(product, Fragment): for atom in product.atoms: if not isinstance(atom, CuttingLabel): products_net_charge += atom.charge product_elements[atom.element] += 1 + elif isinstance(product, Molecule): + for atom in product.atoms: + products_net_charge += atom.charge + product_elements[atom.element] += 1 for element in element_list: if reactant_elements[element] != product_elements[element]: From e304a89958522289962ae6dc38346d8d472dced5 Mon Sep 17 00:00:00 2001 From: ssun30 Date: Fri, 25 Oct 2024 15:18:43 -0400 Subject: [PATCH 105/109] Changed RMS branch to for_rmg For: >> Continuous Integration >> Documentations for compiling from source >> Building the Docker image *The marcus_development branch includes all the commits necessary to make electrochemistry work but doesn't include changes for the RMG-RMS interface overhaul. Will be reverted once that gets merged into RMS main. --- .github/workflows/CI.yml | 4 +++- .github/workflows/docs.yml | 2 +- .../source/users/rmg/installation/anacondaDeveloper.rst | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 19ae1ea7c7..4e2cc7a77d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -175,7 +175,7 @@ jobs: timeout-minutes: 120 # this usually takes 20-45 minutes (or hangs for 6+ hours). run: | python -c "import julia; julia.install(); import diffeqpy; diffeqpy.install()" - julia -e 'using Pkg; Pkg.add(PackageSpec(name="ReactionMechanismSimulator",rev="main")); using ReactionMechanismSimulator' + julia -e 'using Pkg; Pkg.add(PackageSpec(name="ReactionMechanismSimulator",rev="for_rmg")); using ReactionMechanismSimulator' - name: Install Q2DTor run: echo "" | make q2dtor @@ -405,3 +405,5 @@ jobs: with: push: true tags: reactionmechanismgenerator/rmg:latest + build-args: | + RMS_Branch=for_rmg diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 43ab6b0ab1..ef3509d78f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -64,7 +64,7 @@ jobs: timeout-minutes: 120 # this usually takes 20-45 minutes (or hangs for 6+ hours). run: | python -c "import julia; julia.install(); import diffeqpy; diffeqpy.install()" - julia -e 'using Pkg; Pkg.add(PackageSpec(name="ReactionMechanismSimulator",rev="main")); using ReactionMechanismSimulator' + julia -e 'using Pkg; Pkg.add(PackageSpec(name="ReactionMechanismSimulator",rev="for_rmg")); using ReactionMechanismSimulator' - name: Checkout gh-pages Branch uses: actions/checkout@v2 diff --git a/documentation/source/users/rmg/installation/anacondaDeveloper.rst b/documentation/source/users/rmg/installation/anacondaDeveloper.rst index 7dbcfb6677..e1aed5f7db 100644 --- a/documentation/source/users/rmg/installation/anacondaDeveloper.rst +++ b/documentation/source/users/rmg/installation/anacondaDeveloper.rst @@ -117,7 +117,7 @@ Installation by Source Using Anaconda Environment for Unix-based Systems: Linux #. Install and Link Julia dependencies: :: - julia -e 'using Pkg; Pkg.add("PyCall");Pkg.build("PyCall");Pkg.add(PackageSpec(name="ReactionMechanismSimulator",rev="main")); using ReactionMechanismSimulator;' + julia -e 'using Pkg; Pkg.add("PyCall");Pkg.build("PyCall");Pkg.add(PackageSpec(name="ReactionMechanismSimulator",rev="for_rmg")); using ReactionMechanismSimulator;' python -c "import julia; julia.install(); import diffeqpy; diffeqpy.install()" From 36881444e9482daec88e6b1cde33d0c83b9edea1 Mon Sep 17 00:00:00 2001 From: Su Sun Date: Sun, 24 Nov 2024 00:03:27 -0500 Subject: [PATCH 106/109] Use the lithium_rebase branch of RMG-database for CI This is needed for testing. Once lithium_rebase is merged to main on the database, we can go back to using main. --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4e2cc7a77d..1fe3c7e9ef 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -47,7 +47,7 @@ concurrency: env: # if running on RMG-Py but requiring changes on an un-merged branch of RMG-database, replace # main with the name of the branch - RMG_DATABASE_BRANCH: main + RMG_DATABASE_BRANCH: lithium_rebase jobs: From 53059fe12af23cf04372fdffd9a6a3abbeda63ca Mon Sep 17 00:00:00 2001 From: Richard West Date: Sun, 24 Nov 2024 00:42:24 -0500 Subject: [PATCH 107/109] fixup! enable use of non-surface charge transfer families I presume this method was reintroduced by mistake during a rebase. It was removed in f695b059774ed164f203a398f7852a1464f4b6d4 --- rmgpy/data/kinetics/family.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index 7c0ec5405d..a05056a069 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -2636,30 +2636,6 @@ def get_kinetics(self, reaction, template_labels, degeneracy=1, estimator='', re return kinetics_list - def estimate_kinetics_using_group_additivity(self, template, degeneracy=1): - """ - Determine the appropriate kinetics for a reaction with the given - `template` using group additivity. - - Returns just the kinetics, or None. - """ - warnings.warn("Group additivity is no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - # Start with the generic kinetics of the top-level nodes - kinetics = None - root = self.get_root_template() - kinetics = self.get_kinetics_for_template(root) - - if kinetics is None: - # raise UndeterminableKineticsError('Cannot determine group additivity kinetics estimate for ' - # 'template "{0}".'.format(','.join([e.label for e in template]))) - return None - else: - kinetics = kinetics[0] - - # Now add in more specific corrections if possible - return self.groups.estimate_kinetics_using_group_additivity(template, kinetics, degeneracy) - def estimate_kinetics_using_rate_rules(self, template, degeneracy=1): """ Determine the appropriate kinetics for a reaction with the given From aca0b343e9b1f3415d8c85b0d928e99e9c0395b1 Mon Sep 17 00:00:00 2001 From: Richard West Date: Sun, 24 Nov 2024 00:44:10 -0500 Subject: [PATCH 108/109] Small efficiency gain checking for charged species. I couldn't help myself, while doing code review. --- rmgpy/data/kinetics/family.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index a05056a069..43e3836169 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -1701,9 +1701,9 @@ def _create_reaction(self, reactants, products, is_forward): ) if not self.allow_charged_species: - charged_species = [spc for spc in (reaction.reactants + reaction.products) if spc.get_net_charge() != 0] - if charged_species: - return None + for spc in (reaction.reactants + reaction.products): + if spc.get_net_charge() != 0: + return None if not reaction.is_balanced(): return None From 0e524a257743de47e4be8b5bf3e66778ca1dfbb1 Mon Sep 17 00:00:00 2001 From: Richard West Date: Wed, 27 Nov 2024 11:07:38 -0500 Subject: [PATCH 109/109] Switch back to using main branch of RMG-database The PR there was just merged in https://github.com/ReactionMechanismGenerator/RMG-database/pull/667 --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1fe3c7e9ef..4e2cc7a77d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -47,7 +47,7 @@ concurrency: env: # if running on RMG-Py but requiring changes on an un-merged branch of RMG-database, replace # main with the name of the branch - RMG_DATABASE_BRANCH: lithium_rebase + RMG_DATABASE_BRANCH: main jobs: