diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5183002095..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 @@ -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) @@ -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/arkane/encorr/ae.py b/arkane/encorr/ae.py index b06bd025c9..ecef89ae37 100644 --- a/arkane/encorr/ae.py +++ b/arkane/encorr/ae.py @@ -73,7 +73,10 @@ 'Methane', 'Methyl', 'Ammonia', - 'Chloromethane' + 'Chloromethane', + # Lithium species shall be uncommented after we reconcile the difference in AECs and BACs + # 'Lithium Hydride', + # 'Lithium Fluoride' ] 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/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()" 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, +) 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, +) 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'], 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('================================================================================') - 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] 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, }) diff --git a/rmgpy/data/base.py b/rmgpy/data/base.py index 54c0e6c070..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 @@ -1354,8 +1357,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 diff --git a/rmgpy/data/kinetics/database.py b/rmgpy/data/kinetics/database.py index 91b13e3972..6650a532cc 100644 --- a/rmgpy/data/kinetics/database.py +++ b/rmgpy/data/kinetics/database.py @@ -45,11 +45,12 @@ from rmgpy.kinetics import Arrhenius, ArrheniusEP, ThirdBody, Lindemann, Troe, \ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, \ Chebyshev, KineticsData, StickingCoefficient, \ - StickingCoefficientBEP, SurfaceArrhenius, SurfaceArrheniusBEP, ArrheniusBM + StickingCoefficientBEP, SurfaceArrhenius, SurfaceArrheniusBEP, \ + ArrheniusBM, SurfaceChargeTransfer, KineticsModel, Marcus 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, SoluteTSData, SoluteTSDiffData ################################################################################ @@ -80,8 +81,14 @@ def __init__(self): 'StickingCoefficientBEP': StickingCoefficientBEP, 'SurfaceArrhenius': SurfaceArrhenius, 'SurfaceArrheniusBEP': SurfaceArrheniusBEP, + 'SurfaceChargeTransfer': SurfaceChargeTransfer, 'R': constants.R, - 'ArrheniusBM': ArrheniusBM + 'ArrheniusBM': ArrheniusBM, + 'SoluteData': SoluteData, + 'SoluteTSData': SoluteTSData, + 'SoluteTSDiffData': SoluteTSDiffData, + 'KineticsModel': KineticsModel, + 'Marcus': Marcus, } self.global_context = {} diff --git a/rmgpy/data/kinetics/depository.py b/rmgpy/data/kinetics/depository.py index f3765860a9..b299a81152 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 @@ -104,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). @@ -187,6 +212,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/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index f8818c78ac..43e3836169 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 @@ -55,7 +56,8 @@ from rmgpy.exceptions import ActionError, DatabaseError, InvalidActionError, KekulizationError, KineticsError, \ ForbiddenStructureException, UndeterminableKineticsError from rmgpy.kinetics import Arrhenius, SurfaceArrhenius, SurfaceArrheniusBEP, StickingCoefficient, \ - StickingCoefficientBEP, ArrheniusBM + StickingCoefficientBEP, ArrheniusBM, SurfaceChargeTransfer, ArrheniusChargeTransfer, \ + 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 @@ -63,6 +65,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, SoluteTSData, to_soluteTSdata ################################################################################ @@ -102,6 +106,7 @@ def __init__(self, estimator=None, reverse=None, is_forward=None, + electrons=0, ): Reaction.__init__(self, index=index, @@ -115,6 +120,7 @@ def __init__(self, degeneracy=degeneracy, pairs=pairs, is_forward=is_forward, + electrons=electrons ) self.family = family self.template = template @@ -140,7 +146,8 @@ def __reduce__(self): self.template, self.estimator, self.reverse, - self.is_forward + self.is_forward, + self.electrons )) def __repr__(self): @@ -162,6 +169,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 +203,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 @@ -205,6 +214,86 @@ 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) + + + 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 + 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)) + 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.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 self.products: + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data += spc_solute_data*x + + dGTS,dHTS = site_data.calculate_corrections(solvent_data) + dSTS = (dHTS - dGTS)/298.0 + + 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) ################################################################################ @@ -260,6 +349,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': @@ -309,7 +402,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): @@ -383,7 +476,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 +494,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']: @@ -445,8 +542,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: =================== =============================== ======================== @@ -537,11 +634,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. """ @@ -558,6 +655,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 +669,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 @@ -586,6 +687,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) @@ -634,7 +738,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: @@ -673,7 +777,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. ' @@ -700,12 +805,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): @@ -808,7 +913,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')) @@ -825,7 +930,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() @@ -861,10 +966,16 @@ 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)) + 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: @@ -985,14 +1096,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) @@ -1076,6 +1187,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 +1230,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 @@ -1185,7 +1313,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). """ @@ -1350,22 +1478,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: @@ -1383,15 +1512,20 @@ 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(sort_atoms=not self.save_order) - elif isinstance(struct, Fragment): - struct.update() + struct.update_charge() + 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() if label in ['1,2_insertion_co', 'r_addition_com', 'co_disproportionation', 'intra_no2_ono_conversion', 'lone_electron_pair_bond', @@ -1399,20 +1533,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: @@ -1523,10 +1662,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 @@ -1558,8 +1697,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: + for spc in (reaction.reactants + reaction.products): + if spc.get_net_charge() != 0: + 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]): @@ -1570,7 +1718,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. """ @@ -1746,9 +1894,17 @@ 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 + # 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) @@ -1810,7 +1966,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 @@ -2124,7 +2280,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()]) @@ -2194,7 +2350,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: @@ -2378,7 +2534,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. @@ -2484,7 +2640,7 @@ 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. @@ -2495,8 +2651,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) @@ -2509,8 +2665,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 = [] @@ -2521,9 +2677,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 @@ -2702,7 +2858,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 @@ -2727,14 +2883,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 @@ -2758,15 +2914,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 = [[]] @@ -2777,7 +2933,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] @@ -2896,7 +3052,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: @@ -2906,17 +3062,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 @@ -2927,7 +3083,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) @@ -2983,10 +3139,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) @@ -3082,7 +3238,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: @@ -3098,21 +3254,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 @@ -3125,7 +3281,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, @@ -3156,9 +3312,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 @@ -3261,7 +3417,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 @@ -3396,7 +3552,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) @@ -3417,11 +3591,21 @@ 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): + 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] + 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 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)} """ @@ -3473,44 +3657,44 @@ 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 != []: + + 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)) - k = kinetics.get_rate_coefficient(T) - errors[rxn] = np.log(k / krxn) + 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.)) 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(rxn.get_enthalpy_of_reaction(T)) + kinetics = kinetics.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(298.)) + 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 def cross_validate_old(self, folds=5, T=1000.0, random_state=1, estimator='rate rules', thermo_database=None, get_reverse=False, uncertainties=True): @@ -3520,7 +3704,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)) @@ -3564,7 +3748,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: @@ -3574,19 +3758,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: @@ -3703,7 +3887,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, @@ -3792,7 +3976,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: @@ -3802,11 +3986,11 @@ 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 - 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 """ @@ -3863,8 +4047,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 @@ -3876,7 +4060,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 @@ -3894,7 +4078,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 @@ -3906,6 +4090,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) @@ -3924,12 +4110,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)))): @@ -3962,7 +4148,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 @@ -3991,6 +4180,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 @@ -4017,15 +4208,15 @@ 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: + 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: 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 @@ -4112,10 +4303,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(';') @@ -4132,13 +4323,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),...] """ @@ -4146,7 +4337,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: @@ -4220,7 +4411,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()) @@ -4231,11 +4422,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, @@ -4270,7 +4461,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: @@ -4284,7 +4475,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' @@ -4366,8 +4557,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 @@ -4375,29 +4570,115 @@ 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 """ 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) + 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: - kin = ArrheniusBM().fit_to_reactions(rxns, recipe=recipe) + 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): + 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: + # 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]) 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( - ArrheniusBM().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 - varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 + 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() @@ -4405,10 +4686,42 @@ 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: - return None - + 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(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 + 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(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 + 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() @@ -4433,7 +4746,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())) @@ -4444,25 +4757,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 - 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 - 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)' @@ -4481,17 +4778,122 @@ def average_kinetics(kinetics_list): # surface: sticking coefficient pass else: - raise Exception(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,]: - raise Exception(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 False: - pass + 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, + 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), n=n, Ea=(Ea * 0.001, "kJ/mol"), + 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 = to_soluteTSdata(ts_data,reactants=rxn.reactants) + + #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 + + dGrxn = GP-GR + if dGrxn > 0: + x = 1.0 + else: + x = 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 rxn.products: + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data -= spc_solute_data*x + + return site_data + else: + return None diff --git a/rmgpy/data/kinetics/library.py b/rmgpy/data/kinetics/library.py index 747c37d746..4298084a8c 100644 --- a/rmgpy/data/kinetics/library.py +++ b/rmgpy/data/kinetics/library.py @@ -43,12 +43,13 @@ 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, Marcus 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 ################################################################################ @@ -219,6 +220,52 @@ 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) + + 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 + + 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 = 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) ################################################################################ diff --git a/rmgpy/data/kinetics/rules.py b/rmgpy/data/kinetics/rules.py index 6f20e5460a..d20a4a6ce4 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, Marcus 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): 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/solvation.py b/rmgpy/data/solvation.py index 45a133de01..b95f708f02 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, name_in_coolprop=None, n=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 @@ -470,7 +473,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 +494,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 +513,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. @@ -532,6 +535,299 @@ 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 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) + +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. @@ -859,7 +1155,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 +1450,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 +1488,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. @@ -1311,6 +1607,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}) @@ -1921,7 +2218,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 @@ -2132,7 +2429,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 @@ -2150,7 +2447,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") diff --git a/rmgpy/data/thermo.py b/rmgpy/data/thermo.py index 7816d7ed2f..34f9630dac 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()): @@ -854,6 +857,7 @@ def __init__(self): self.libraries = {} self.surface = {} self.groups = {} + self.adsorption_groups = "adsorptionPt111" self.library_order = [] self.local_context = { 'ThermoData': ThermoData, @@ -989,7 +993,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 +1293,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", @@ -1481,10 +1489,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 @@ -1495,8 +1507,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., '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: @@ -1526,11 +1538,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 @@ -1607,7 +1622,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) @@ -2049,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 @@ -2555,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/kinetics/__init__.py b/rmgpy/kinetics/__init__.py index 0816d655f7..b3f8eec12e 100644 --- a/rmgpy/kinetics/__init__.py +++ b/rmgpy/kinetics/__init__.py @@ -29,10 +29,12 @@ 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, Marcus from rmgpy.kinetics.chebyshev import Chebyshev from rmgpy.kinetics.falloff import ThirdBody, Lindemann, Troe 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/arrhenius.pxd b/rmgpy/kinetics/arrhenius.pxd index 21e44a3be3..2d39ac48d4 100644 --- a/rmgpy/kinetics/arrhenius.pxd +++ b/rmgpy/kinetics/arrhenius.pxd @@ -128,4 +128,74 @@ 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 + + +################################################################################ + +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) + +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 f24defb8cc..f4b69179ed 100644 --- a/rmgpy/kinetics/arrhenius.pyx +++ b/rmgpy/kinetics/arrhenius.pyx @@ -37,6 +37,8 @@ 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 RCOND = -1 if int(np.__version__.split('.')[1]) < 14 else None @@ -58,15 +60,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 +86,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 +96,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 +200,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 +306,7 @@ cdef class Arrhenius(KineticsModel): Pmin=self.Pmin, Pmax=self.Pmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment) return aep ################################################################################ @@ -323,15 +329,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 +355,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 +365,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 +436,7 @@ cdef class ArrheniusEP(KineticsModel): Pmin=self.Pmin, Pmax=self.Pmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment, ) @@ -481,15 +490,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 +516,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 +526,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.""" @@ -549,7 +560,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) @@ -560,7 +571,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 @@ -576,7 +587,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, @@ -586,6 +598,7 @@ cdef class ArrheniusBM(KineticsModel): Tmin=self.Tmin, Tmax=self.Tmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment, ) @@ -593,6 +606,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' @@ -604,28 +620,25 @@ 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(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.comment = 'Fitted to {0} reaction at temperature: {1} K'.format(len(rxns), T) + self.solute = None + self.comment = 'Fitted to 1 reaction.' else: - # define optimization function + # define optimization function def kfcn(xs, lnA, n, E0): T = xs[:,0] dHrxn = xs[:,1] @@ -634,7 +647,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 = [] @@ -643,25 +656,24 @@ 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)) 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: @@ -672,11 +684,14 @@ 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 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 @@ -1097,6 +1112,759 @@ 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 + +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 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' + + 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: + rxn = rxns[0] + 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 + + E0 = fsolve(kfcn, w0 / 10.0)[0] + + self.Tmin = rxn.kinetics.Tmin + self.Tmax = rxn.kinetics.Tmax + self.comment = 'Fitted to 1 reaction' + 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(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 + keep_trying = True + xtol = 1e-8 + ftol = 1e-8 + 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: + keep_trying = 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) + 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 + 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.') + +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) @@ -1167,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/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..262b33142b 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 @@ -137,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.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.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.""" 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/molecule/adjlist.py b/rmgpy/molecule/adjlist.py index 1ebe0ff110..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'}: + 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.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..960648c6ee 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,8 @@ def get_features(self): # Non-surface atomTypes, R being the most generic: ATOMTYPES['R'] = AtomType(label='R', generic=['Rx'], specific=[ - 'H', + 'H','H0','H+', + 'Li','Li0','Li+', 'R!H', 'R!H!Val7', 'Val4','Val5','Val6','Val7', @@ -309,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', @@ -323,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', @@ -349,7 +365,19 @@ 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['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=[]) @@ -670,161 +698,168 @@ 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['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['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=[]) + +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['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=[]) + +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', 'Li', '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..6ec975fcf9 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,14 +122,15 @@ 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, - '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, - '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, - 'Si': 0, 'P': 1, 'S': 2, 'Cl': 3, 'Br': 3, 'Ar': 4, 'I': 3, 'X': 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, '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, '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, '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} ################################################################################ @@ -173,6 +174,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 +313,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, @@ -332,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'), @@ -375,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/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(): diff --git a/rmgpy/molecule/fragment.py b/rmgpy/molecule/fragment.py index 113628e8b1..6e5c10652c 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 @@ -388,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): @@ -657,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(): 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 f4b0649c0d..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 ################################################################################ @@ -80,7 +81,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 +116,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): @@ -271,6 +272,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 +391,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': @@ -357,7 +410,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 +506,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): @@ -550,6 +603,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. @@ -606,6 +671,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: @@ -686,6 +759,7 @@ def make_sample_atom(self): 'I': 3, 'Ar': 4, 'X': 0, + 'e': 0 } for element_label in allElements: @@ -863,7 +937,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+ """ @@ -940,7 +1014,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 @@ -988,7 +1062,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 +1188,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 =================== =================== ==================================== @@ -1255,6 +1329,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 @@ -1337,6 +1419,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. @@ -1565,10 +1649,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 +1716,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 +1826,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 @@ -1770,7 +1854,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] @@ -2067,7 +2151,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 +2167,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,10 +2983,24 @@ 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 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: 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) diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index 03727be792..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 ################################################################################ @@ -354,6 +355,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 @@ -382,6 +400,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 @@ -503,6 +528,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. @@ -544,6 +581,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 @@ -566,6 +607,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': @@ -1175,6 +1220,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 @@ -1221,17 +1274,22 @@ def sort_atoms(self): for index, vertex in enumerate(self.vertices): vertex.sorting_label = index + def update_charge(self): + + for atom in self.atoms: + if not isinstance(atom, CuttingLabel): + 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: @@ -1810,7 +1868,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 @@ -2007,7 +2065,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 @@ -2270,10 +2328,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): """ @@ -2303,7 +2366,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() or atom1.is_lithium(): atom1.lone_pairs = 0 else: order = atom1.get_total_bond_order() diff --git a/rmgpy/molecule/translator.py b/rmgpy/molecule/translator.py index 731d8d8d8e..fece53b041 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' } @@ -506,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): @@ -518,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)) 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']) diff --git a/rmgpy/reaction.pxd b/rmgpy/reaction.pxd index 7755ba2311..15feecef3f 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,29 +85,37 @@ 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=?) + 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=?) @@ -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..19136c60fa 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` @@ -53,14 +53,15 @@ 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, Marcus 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,28 @@ 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. - """ - cython.declare(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) + 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, + 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) + 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 +828,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 +843,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 +860,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 +873,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 +891,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 +943,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): @@ -834,26 +982,29 @@ 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 - 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, 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 - 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 +1013,67 @@ 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, 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, 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 + 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 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): """ @@ -910,6 +1095,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): @@ -933,6 +1119,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): @@ -959,18 +1146,70 @@ 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): + """ + 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=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): + """ + 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) + kr.solute = kf.solute 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 +1217,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__, @@ -986,6 +1226,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 @@ -993,15 +1234,22 @@ 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, ArrheniusChargeTransfer): + return self.reverse_arrhenius_charge_transfer_rate(kf, kunits, Tmin, Tmax) + + elif isinstance(kf, KineticsData): Tlist = kf.Tdata.value_si klist = np.zeros_like(Tlist) @@ -1020,7 +1268,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 +1397,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 +1422,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 +1438,43 @@ 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: - 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] 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: - 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]: return False + if self.electrons < 0: + reactants_net_charge += self.electrons + elif self.electrons > 0: + products_net_charge -= self.electrons + return True def generate_pairs(self): @@ -1222,7 +1482,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 +1491,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 +1555,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 +1567,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)): diff --git a/rmgpy/rmg/input.py b/rmgpy/rmg/input.py index e04cd82e2e..117c19644c 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 ################################################################################ @@ -67,6 +68,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 +103,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, @@ -631,7 +634,10 @@ def liquid_cat_reactor(temperature, initialConcentrations, initialSurfaceCoverages, surfaceVolumeRatio, - potential=None, + distance=None, + viscosity=None, + surfPotential=None, + liqPotential=None, terminationConversion=None, terminationTime=None, terminationRateRatio=None, @@ -707,11 +713,19 @@ 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 + 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 = [] @@ -1136,11 +1150,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, @@ -1539,6 +1560,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 8df771980d..fb4ae33ef8 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 @@ -406,7 +408,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 +494,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 @@ -549,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() @@ -582,7 +585,25 @@ def initialize(self, **kwargs): # Do all liquid-phase startup things: if self.solvent: - solvent_data = self.database.solvation.get_solvent_data(self.solvent) + 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 self.reaction_model.core.phase_system.phases["Default"].set_solvent(solvent_data) self.reaction_model.edge.phase_system.phases["Default"].set_solvent(solvent_data) @@ -615,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: @@ -658,9 +687,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 diff --git a/rmgpy/rmg/model.py b/rmgpy/rmg/model.py index 7c2f5c5c88..ee6795806b 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) @@ -814,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 @@ -1689,7 +1694,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 diff --git a/rmgpy/rmg/reactors.py b/rmgpy/rmg/reactors.py index c487601f93..9386fa2a20 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: @@ -55,18 +56,19 @@ 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, Marcus from rmgpy.kinetics.kineticsdata import KineticsData 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 @@ -488,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, @@ -590,6 +597,28 @@ 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) + n = obj._n.value_si + Ea = obj._Ea.value_si + q = obj._alpha.value_si*obj._electrons.value_si + 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) + n = obj._n.value_si + Ea = obj._Ea.value_si + q = obj._alpha.value_si*obj._electrons.value_si + 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 + 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] @@ -694,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) @@ -752,22 +781,12 @@ 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(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 = 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/species.pxd b/rmgpy/species.pxd index 9922c1e59a..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 @@ -78,6 +80,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..2969dac82b 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 @@ -358,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 @@ -495,6 +503,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 diff --git a/rmgpy/yml.py b/rmgpy/yml.py index cd8b9de5b5..8b6e9f771f 100644 --- a/rmgpy/yml.py +++ b/rmgpy/yml.py @@ -34,17 +34,18 @@ 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 +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 -from rmgpy.kinetics.surface import StickingCoefficient +from rmgpy.kinetics.surface import StickingCoefficient, SurfaceChargeTransfer from rmgpy.util import make_output_subdirectory @@ -141,6 +142,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) @@ -148,6 +150,30 @@ 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): + 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, 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" diff --git a/test/database/databaseTest.py b/test/database/databaseTest.py index 3011ba17eb..3663af56f0 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 @@ -129,11 +130,17 @@ 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_Alpha_vdW", + "Surface_Proton_Electron_Reduction_Beta", + "Surface_Proton_Electron_Reduction_Beta_vdW", + "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 @@ -970,7 +977,7 @@ 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) @@ -1519,7 +1526,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, """ @@ -1976,7 +1983,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/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/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..5aeaf0b746 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,314 @@ 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)) + 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): + + 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 b980621d0c..1a6c3d2879 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,128 @@ 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. + """ + 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) + # 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) + #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) + # 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) + #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. @@ -272,6 +396,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): """ @@ -362,6 +500,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. @@ -482,6 +636,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']]) + assert electron.is_electron() + + def test_is_proton(self): + """ + Test the GroupAtom.is_proton() method. + """ + proton = GroupAtom(atomtype=[ATOMTYPES['H+']]) + assert proton.is_proton() class TestGroupBond: """ @@ -510,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): """ @@ -666,6 +862,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 +1000,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. + """ + 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_get_labeled_atom(self): """ Test the Group.get_labeled_atoms() method. diff --git a/test/rmgpy/molecule/moleculeTest.py b/test/rmgpy/molecule/moleculeTest.py index 33e75dec58..f9ee25000d 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': + assert atom.is_hydrogen() + assert atom.is_proton() + atom.charge = 0 + assert not atom.is_proton() + else: + assert not 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': + assert atom.is_electron() + else: + assert not atom.is_electron() + def test_is_non_hydrogen(self): """ Test the Atom.is_non_hydrogen() method. @@ -396,6 +421,36 @@ 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) + 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) + assert atom0.charge == atom.charge - 1 + assert 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) + 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) + assert atom0.charge == atom.charge + 1 + assert atom0.label == atom.label + def test_equivalent(self): """ Test the Atom.equivalent() method. @@ -946,6 +1001,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""") + 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""") + assert electron.is_electron() + def test_equality(self): """Test that we can perform equality comparison with Molecule objects""" assert self.mol1 == self.mol1 diff --git a/test/rmgpy/reactionTest.py b/test/rmgpy/reactionTest.py index 26441c9e78..4e1cc0cc50 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""" + assert self.rxn1s.electrons == 0 + assert self.rxn1s.electrons == 0 + + def test_protons(self): + """Test protons property""" + 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""" @@ -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""" + assert self.rxn_reduction.electrons == -1 + assert self.rxn_oxidation.electrons == 1 + + def test_protons(self): + """Test n_protons property""" + assert self.rxn_reduction.protons == -1 + assert self.rxn_oxidation.protons == 1 + + def test_is_surface_reaction(self): + """Test is_surface_reaction() method""" + 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""" + 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""" + 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) + + 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""" + + # 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) + + 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) + assert 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)): + 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) + assert abs(Kc_oxidation_equil * Kc_reduction_equil - 1.0) < 0.0001 + C0 = 1e5 / constants.R / 298 + assert abs(Kc_oxidation_equil - C0) < 0.0001 + assert abs(Kc_reduction_equil - 1/C0) < 0.0001 + + @pytest.mark.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() + 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: + 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) + 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) + assert order_of_magnitude(kf/kr) == order_of_magnitude(K) diff --git a/test/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 new file mode 100644 index 0000000000..656846b7f4 --- /dev/null +++ b/test/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/test/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 new file mode 100644 index 0000000000..67a06429f2 --- /dev/null +++ b/test/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/test/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 new file mode 100644 index 0000000000..c4d8a3f3e8 --- /dev/null +++ b/test/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/test/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 new file mode 100644 index 0000000000..3866fa74ac --- /dev/null +++ b/test/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, +# ) 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", +)