diff --git a/.travis.yml b/.travis.yml index 1d7e46b9ba..f076478ead 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: python sudo: false python: - "2.7" -virtualenv: - system_site_packages: true env: global: - secure: "L2ja+ZnV83w4qG3E8FwTjm0D6IWNOnj5wuFOjYTwbzQP4OAgLAWBzCMtxzWy5sMxFLtRgkswBH1d5f5kg8Ab7GIyAMFgQwe8UFqMJ+N05QNszE1mJkAvJtv2XN7669XXQhTt5EXfHrCcGZaODVnI2CEA8GB5DxiHO2Lcqf/xvgE=" @@ -12,29 +10,24 @@ env: - secure: "cfosGf5hvUhIlPoGJu0d/HFddrMwIFU7FfLwd8yRrMGkYv0ePOwAW9kmhFSxUYvuXkxzgD75cIICMFY2fSm6VXBXXzfPQD7vwzoApXf7a8vi0C64XhinXhdEyUYb5/v8fswa0zheUENYhUS1tOqDXT/h8EPNZT5wKizaA3O2Wa8=" - secure: "QXuqKYuwCocqsTMePBc5OugBbQC4/t+335TYLdkletiateP/rF/eDsVRG792/BVq5gKRZgz3NH9ipTNm5pZoCbAEPt9+eDpfts8WeAbxmjdcEjfBxxwZ69wUTPAVrezTGn2k7W2UBdFrWeUNKPAVCKIkoviXqOHFitqJEC+c6JY=" - secure: "jIyBEzR10l5SWvY5ouEYzA8YzPHIZNMXMBdcXwuwte8NCU8GBYUqhHA1L67nTaBdLhWbrZ2NireVKPQWJp3ctcI0IB6xZzaYlVpgN/udGPO+1MZd9Xhp9TWuJWrGZ9EoWGB9L5H+O7RYwcDMVH5CUrCIBdsSJuyE8aDpky1/IVE=" -addons: - apt: - packages: - - git before_install: - # Set up anaconda - - wget http://repo.continuum.io/miniconda/Miniconda2-4.0.5-Linux-x86_64.sh -O miniconda.sh - - chmod +x miniconda.sh - - ./miniconda.sh -b -p $HOME/miniconda + - cd .. + # Install miniconda + - wget http://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh + - bash miniconda.sh -b -p $HOME/miniconda - export PATH=$HOME/miniconda/bin:$PATH - - export PYTHONPATH=$TRAVIS_BUILD_DIR/RMG-Py:$PYTHONPATH - # Update conda itself - conda update --yes conda - - cd .. + - conda info -a + # Clone RMG-database - git clone https://github.com/ReactionMechanismGenerator/RMG-database.git - cd RMG-Py install: - - conda env create -f environment_linux.yml + - conda env create -q -f environment_linux.yml - source activate rmg_env + - conda install -y -c conda-forge codecov - conda list - - pip install codecov - yes 'Yes' | $HOME/miniconda/envs/rmg_env/bin/mopac $MOPACKEY > /dev/null - make diff --git a/documentation/source/users/rmg/database/kinetics.rst b/documentation/source/users/rmg/database/kinetics.rst index 09c3c6d661..2bc6fe7f0c 100644 --- a/documentation/source/users/rmg/database/kinetics.rst +++ b/documentation/source/users/rmg/database/kinetics.rst @@ -56,101 +56,129 @@ Below is a list of pre-packaged kinetics library reactions in RMG: -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Library |Description | -+=======================================+==========================================================================================+ -|1989_Stewart_2CH3_to_C2H5_H |Chemically Activated Methyl Recombination to Ethyl (2CH3 -> C2H5 + H) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|2001_Tokmakov_H_Toluene_to_CH3_Benzene |H + Toluene = CH3 + Benzene | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|2005_Senosiain_OH_C2H2 |pathways on the OH + acetylene surface | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|2006_Joshi_OH_CO |pathways on OH + CO = HOCO = H + CO2 surface | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|BurkeH2O2inArHe |Comprehensive H2/O2 kinetic model in Ar or He atmosphere | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|BurkeH2O2inN2 |Comprehensive H2/O2 kinetic model in N2 atmosphere | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|C2H4+O_Klipp2017 |C2H4 + O intersystem crossing reactions, probably important for all C/H/O combustion | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|C10H11 |Cyclopentadiene pyrolysis in the presence of ethene | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|C3 |Cyclopentadiene pyrolysis in the presence of ethene | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|C6H5_C4H4_Mebel |Formation Mechanism of Naphthalene and Indene | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Chernov |Soot Formation with C1 and C2 Fuels (aromatic reactions only) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|CurranPentane |Ignition of pentane isomers | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Dooley |Methyl formate (contains several mechanisms) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|ERC-FoundationFuelv0.9 |Small molecule combustio (natural gas) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Ethylamine |Ethylamine pyrolysis and oxidation | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|FFCM1(-) |Foundational Fuel Chemistry Model Version 1.0 (excited species removed) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Fulvene_H |Cyclopentadiene pyrolysis in the presence of ethene | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|GRI-HCO |The `HCO <=> H + CO` reaction | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|GRI-Mech3.0 |Gas Research Institute natural gas mechanism optimized for 1 atm (discountinued Feb. 2000)| -+---------------------------------------+------------------------------------------------------------------------------------------+ -|GRI-Mech3.0-N |GRI-Mech3.0 including nitrogen chemistry (NOx from N2) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Glarborg |Mechanisms by P. Glarborg, assorted by carbon number | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|JetSurF2.0 |Jet Surrogate Fuel model up tp C12 (excited species removed) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Klippenstein_Glarborg2016 |Methane oxidation at high pressures and intermediate temperatures | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Mebel_C6H5_C2H2 |Pathways from benzene to naphthalene | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Mebel_Naphthyl |Reactions of naphthyl-1 and naphthyl-2 | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Methylformate |Methyl formate | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Narayanaswamy |Oxidation of substituted aromatic species (aromatic reactions only) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Nitrogen_Dean_and_Bozzelli |Combustion Chemistry of Nitrogen | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Nitrogen_Glarborg_Gimenez_et_al |High pressure C2H4 oxidation with nitrogen chemistry | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Nitrogen_Glarborg_Lucassen_et_al |Fuel-nitrogen conversion in the combustion of small amines | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Nitrogen_Glarborg_Zhang_et_al |Premixed nitroethane flames at low pressure | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|OxygenSingTrip |Reactions of singlet and triplet oxygen | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Sulfur/DMDS |Dimethyl disulfide (CH3SSCH3) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Sulfur/DMS |Dimethyl disulfide (CH3SSCH3) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Sulfur/DTBS |Di-tert-butyl Sulfide (C4H9SSC4H9) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Sulfur/Hexanethial_nr |Hexyl sulfide (C6H13SC6H13) + hexadecane (C16H34) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Sulfur/Sendt |Small sulfur molecule | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Sulfur/TP_Song |Thiophene (C4H4S, aromatic) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|Sulfur/Thial_Hydrolysis |Thioformaldehyde (CH2S) and thioacetaldehyde (C2H4S) to COS and CO2 | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|TEOS |Organic oxidized silicone | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|c-C5H5_CH3_Sharma |Cyclopentadienyl + CH3 | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|combustion_core |Leeds University natural gas mechanism (contains versions 2-5) | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|fascella |Cyclopentadienyl + acetyl | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|kislovB |Formation of indene in combustion | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|naphthalene_H |Cyclopentadiene pyrolysis in the presence of ethene Part 1 | -+---------------------------------------+------------------------------------------------------------------------------------------+ -|vinylCPD_H |Cyclopentadiene pyrolysis in the presence of ethene Part 2 | -+---------------------------------------+------------------------------------------------------------------------------------------+ ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Library |Description | ++=============================================================+==========================================================================================+ +|1989_Stewart_2CH3_to_C2H5_H |Chemically Activated Methyl Recombination to Ethyl (2CH3 -> C2H5 + H) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|2001_Tokmakov_H_Toluene_to_CH3_Benzene |H + Toluene = CH3 + Benzene | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|2005_Senosiain_OH_C2H2 |pathways on the OH + acetylene surface | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|2006_Joshi_OH_CO |pathways on OH + CO = HOCO = H + CO2 surface | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|2009_Sharma_C5H5_CH3_highP |Cyclopentadienyl + CH3 in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|2015_Buras_C2H3_C4H6_highP |Vinyl + 1,3-Butadiene and other C6H9 reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|biCPD_H_shift |Sigmatropic 1,5-H shifts on biCPD PES | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|BurkeH2O2inArHe |Comprehensive H2/O2 kinetic model in Ar or He atmosphere | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|BurkeH2O2inN2 |Comprehensive H2/O2 kinetic model in N2 atmosphere | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|C2H4+O_Klipp2017 |C2H4 + O intersystem crossing reactions, probably important for all C/H/O combustion | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|C10H11 |Cyclopentadiene pyrolysis in the presence of ethene | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|C3 |Cyclopentadiene pyrolysis in the presence of ethene | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|C6H5_C4H4_Mebel |Formation Mechanism of Naphthalene and Indene | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Chernov |Soot Formation with C1 and C2 Fuels (aromatic reactions only) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|CurranPentane |Ignition of pentane isomers | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Dooley |Methyl formate (contains several mechanisms) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|ERC-FoundationFuelv0.9 |Small molecule combustio (natural gas) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Ethylamine |Ethylamine pyrolysis and oxidation | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|FFCM1(-) |Foundational Fuel Chemistry Model Version 1.0 (excited species removed) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2005_Ismail_C6H5_C4H6_highP |Phenyl + 1,3-Butadiene and other C10H11 reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2012_Matsugi_C3H3_C7H7_highP |Propargyl + Benzyl and other C10H10 reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2016_Mebel_C9H9_highP |C9H9 reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2016_Mebel_C10H9_highP |C10H9 reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2016_Mebel_Indene_CH3_highP |CH3 + Indene in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2017_Buras_C6H5_C3H6_highP |Phenyl + Propene and other C9H11 reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2017_Mebel_C6H4C2H_C2H2_highP |C10H7 HACA reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2017_Mebel_C6H5_C2H2_highP |C8H7 HACA reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2017_Mebel_C6H5_C4H4_highP |Phenyl + Vinylacetylene and other C10H9 reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/2017_Mebel_C6H5C2H2_C2H2_highP |C10H9 HACA reactions in high-P limit | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|First_to_Second_Aromatic_Ring/phenyl_diacetylene_effective |Effective Phenyl + Diacetylene rates to Benzofulvenyl and 2-Napthyl | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Fulvene_H |Cyclopentadiene pyrolysis in the presence of ethene | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|GRI-HCO |The `HCO <=> H + CO` reaction | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|GRI-Mech3.0 |Gas Research Institute natural gas mechanism optimized for 1 atm (discountinued Feb. 2000)| ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|GRI-Mech3.0-N |GRI-Mech3.0 including nitrogen chemistry (NOx from N2) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Glarborg |Mechanisms by P. Glarborg, assorted by carbon number | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|JetSurF2.0 |Jet Surrogate Fuel model up tp C12 (excited species removed) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Klippenstein_Glarborg2016 |Methane oxidation at high pressures and intermediate temperatures | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Mebel_C6H5_C2H2 |Pathways from benzene to naphthalene | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Mebel_Naphthyl |Reactions of naphthyl-1 and naphthyl-2 | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Methylformate |Methyl formate | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Narayanaswamy |Oxidation of substituted aromatic species (aromatic reactions only) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Nitrogen_Dean_and_Bozzelli |Combustion Chemistry of Nitrogen | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Nitrogen_Glarborg_Gimenez_et_al |High pressure C2H4 oxidation with nitrogen chemistry | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Nitrogen_Glarborg_Lucassen_et_al |Fuel-nitrogen conversion in the combustion of small amines | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Nitrogen_Glarborg_Zhang_et_al |Premixed nitroethane flames at low pressure | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|OxygenSingTrip |Reactions of singlet and triplet oxygen | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Sulfur/DMDS |Dimethyl disulfide (CH3SSCH3) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Sulfur/DMS |Dimethyl disulfide (CH3SSCH3) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Sulfur/DTBS |Di-tert-butyl Sulfide (C4H9SSC4H9) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Sulfur/Hexanethial_nr |Hexyl sulfide (C6H13SC6H13) + hexadecane (C16H34) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Sulfur/Sendt |Small sulfur molecule | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Sulfur/TP_Song |Thiophene (C4H4S, aromatic) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|Sulfur/Thial_Hydrolysis |Thioformaldehyde (CH2S) and thioacetaldehyde (C2H4S) to COS and CO2 | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|TEOS |Organic oxidized silicone | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|c-C5H5_CH3_Sharma |Cyclopentadienyl + CH3 | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|combustion_core |Leeds University natural gas mechanism (contains versions 2-5) | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|fascella |Cyclopentadienyl + acetyl | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|kislovB |Formation of indene in combustion | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|naphthalene_H |Cyclopentadiene pyrolysis in the presence of ethene Part 1 | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ +|vinylCPD_H |Cyclopentadiene pyrolysis in the presence of ethene Part 2 | ++-------------------------------------------------------------+------------------------------------------------------------------------------------------+ diff --git a/documentation/source/users/rmg/releaseNotes.rst b/documentation/source/users/rmg/releaseNotes.rst index 4f0a316598..f0c2dd920c 100644 --- a/documentation/source/users/rmg/releaseNotes.rst +++ b/documentation/source/users/rmg/releaseNotes.rst @@ -4,6 +4,47 @@ Release Notes ************* +RMG-Py Version 2.1.6 +==================== +Date: December 21, 2017 + +- Model resurrection: + - Automatically attempts to save simulation after encountering a DASPK error + - Adds species and reactions in order to modify model dynamics and fix the error + +- New features: + - Add functionality to read RCCSD(T)-F12 energies from MolPro log files + - Add liquidReactor support to flux diagram generation + +- Other changes: + - Removed rmgpy.rmg.model.Species class and merged functionality into main rmgpy.species.Species class + - Refactored parsing of RMG-generated kinetics comments from Chemkin files and fixed related issues + - Refactored framework for generating reactions to reduce code duplication + - Resonance methods renamed from generateResonanceIsomers to generate_resonance_structures across all modules + - Raise CpInf to Cphigh for entropy calculations to prevent invalid results + +- Fixes: + - Update sensitivity analysis to use ModelSettings and SimulatorSettings classes introduced in v2.1.5 + - Fixed generate_reactions methods in KineticsDatabase to be directly usable again + - Fixed issues with aromaticity perception and generation of aromatic resonance structures + +RMG-database Version 2.1.6 +========================== +Date: December 21, 2017 + +- Additions: + - New training reactions added for [NH2] related H_Abstractions + - 14 new kinetics libraries related to aromatics formation (see RMG-database #222 for details) + +- Other changes: + - Removed some global forbidden groups which are no longer needed + - Forbid CO and CS biradicals + - Updated lone_electron_pair_bond family and removed from recommended list + +- Fixes: + - Fixed unit errors in some H_Abstraction and R_Addition_MultipleBond depositories + + RMG-Py Version 2.1.5 ==================== Date: October 18, 2017 diff --git a/examples/rmg/MR_test/input.py b/examples/rmg/MR_test/input.py new file mode 100644 index 0000000000..463e60e5ea --- /dev/null +++ b/examples/rmg/MR_test/input.py @@ -0,0 +1,314 @@ +#This syngas example is for testing Model Resurrection handling (recovery from solver errors) +#The parameters chosen for this example were chosen to generate solver errors +#(so this is not a good example to base an input file for a real job on) +database( + #overrides RMG thermo calculation of RMG with these values. + #libraries found at http://rmg.mit.edu/database/thermo/libraries/ + #if species exist in multiple libraries, the earlier libraries overwrite the + #previous values + thermoLibraries = ['primaryThermoLibrary'], + #overrides RMG kinetics estimation if needed in the core of RMG. + #list of libraries found at http://rmg.mit.edu/database/kinetics/libraries/ + #libraries can be input as either a string or tuple of form ('library_name',True/False) + #where a `True` indicates that all unused reactions will be automatically added + #to the chemkin file at the end of the simulation. Placing just string values + #defaults the tuple to `False`. The string input is sufficient in almost + #all situations + reactionLibraries = [], + #seed mechanisms are reactionLibraries that are forced into the initial mechanism + #in addition to species listed in this input file. + #This is helpful for reducing run time for species you know will appear in + #the mechanism. + seedMechanisms = [], + #this is normally not changed in general RMG runs. Usually used for testing with + #outside kinetics databases + kineticsDepositories = 'default', + #lists specific families used to generate the model. 'default' uses a list of + #families from RMG-Database/input/families/recommended.py + #a visual list of families is available in PDF form at RMG-database/families + kineticsFamilies = 'default', + #specifies how RMG calculates rates. currently, the only option is 'rate rules' + kineticsEstimator = 'rate rules', +) + +# List of species +#list initial and expected species below to automatically put them into the core mechanism. +#'structure' can utilize method of SMILES("put_SMILES_here"), +#adjacencyList("""put_adj_list_here"""), or InChI("put_InChI_here") +#for molecular oxygen, use the smiles string [O][O] so the triplet form is used +species( + label='N2', + reactive=False, + structure=SMILES("N#N") +) +species( + label='CO', + reactive=True, + structure=SMILES("[C-]#[O+]") +) + +species( + label='H2', + reactive=True, + structure=SMILES("[H][H]"), +) +species( + label='O2', + reactive=True, + structure=SMILES("[O][O]") + ) + +#Reaction systems +#currently RMG models only constant temperature and pressure as homogeneous batch reactors. +#two options are: simpleReactor for gas phase or liquidReactor for liquid phase +#use can use multiple reactors in an input file for each condition you want to test. + +simpleReactor( + #specifies reaction temperature with units + temperature=(1000,'K'), + #specifies reaction pressure with units + pressure=(10.0,'bar'), + #list initial mole fractions of compounds using the label from the 'species' label. + #RMG will normalize if sum/=1 + initialMoleFractions={ + "CO": .6, + "H2": .4, + "O2": .5, + "N2": 0, + }, + #the following two values specify when to determine the final output model + #only one must be specified + #the first condition to be satisfied will terminate the process + + terminationTime=(12,'s'), +) + + + +simpleReactor( + #specifies reaction temperature with units + temperature=(1000,'K'), + #specifies reaction pressure with units + pressure=(100.0,'bar'), + #list initial mole fractions of compounds using the label from the 'species' label. + #RMG will normalize if sum/=1 + initialMoleFractions={ + "CO": .6, + "H2": .4, + "O2": .5, + "N2": 0, + }, + #the following two values specify when to determine the final output model + #only one must be specified + #the first condition to be satisfied will terminate the process + + terminationTime=(12,'s'), +) + + + + + +simpleReactor( + #specifies reaction temperature with units + temperature=(2000,'K'), + #specifies reaction pressure with units + pressure=(100.0,'bar'), + #list initial mole fractions of compounds using the label from the 'species' label. + #RMG will normalize if sum/=1 + initialMoleFractions={ + "CO": .6, + "H2": .4, + "O2": .5, + "N2": 0, + }, + #the following two values specify when to determine the final output model + #only one must be specified + #the first condition to be satisfied will terminate the process + + terminationTime=(12,'s'), + ) +simpleReactor( + #specifies reaction temperature with units + temperature=(2000,'K'), + #specifies reaction pressure with units + pressure=(10.0,'bar'), + #list initial mole fractions of compounds using the label from the 'species' label. + #RMG will normalize if sum/=1 + initialMoleFractions={ + "CO": .6, + "H2": .4, + "O2": .5, + "N2": 0, + }, + #the following two values specify when to determine the final output model + #only one must be specified + #the first condition to be satisfied will terminate the process + + terminationTime=(12,'s'), + ) + + + + +#1500 +simpleReactor( + #specifies reaction temperature with units + temperature=(1500,'K'), + #specifies reaction pressure with units + pressure=(100.0,'bar'), + #list initial mole fractions of compounds using the label from the 'species' label. + #RMG will normalize if sum/=1 + initialMoleFractions={ + "CO": .6, + "H2": .4, + "O2": .5, + "N2": 0, + }, + #the following two values specify when to determine the final output model + #only one must be specified + #the first condition to be satisfied will terminate the process + + terminationTime=(12,'s'), + ) +simpleReactor( + #specifies reaction temperature with units + temperature=(1500,'K'), + #specifies reaction pressure with units + pressure=(10.0,'bar'), + #list initial mole fractions of compounds using the label from the 'species' label. + #RMG will normalize if sum/=1 + initialMoleFractions={ + "CO": .6, + "H2": .4, + "O2": .5, + "N2": 0, + }, + #the following two values specify when to determine the final output model + #only one must be specified + #the first condition to be satisfied will terminate the process + + terminationTime=(12,'s'), + ) + +#determines absolute and relative tolerances for ODE solver and sensitivities. +#normally this doesn't cause many issues and is modified after other issues are +#ruled out +simulator( + atol=1e-16, + rtol=1e-8, +# sens_atol=1e-6, +# sens_rtol=1e-4, +) + +#used to add species to the model and to reduce memory usage by removing unimportant additional species. +#all relative values are normalized by a characteristic flux at that time point +model( + #determines the relative flux to put a species into the core. + #A smaller value will result in a larger, more complex model + #when running a new model, it is recommended to start with higher values and then decrease to converge on the model + toleranceMoveToCore=0.01, + #comment out the next three terms to disable pruning + #determines the relative flux needed to not remove species from the model. + #Lower values will keep more species and utilize more memory + toleranceKeepInEdge=0.0, + #determines when to stop a ODE run to add a species. + #Lower values will improve speed. + #if it is too low, may never get to the end simulation to prune species. + toleranceInterruptSimulation=0.01, + #number of edge species needed to accumulate before pruning occurs + #larger values require more memory and will prune less often + maximumEdgeSpecies=100000, + #minimum number of core species needed before pruning occurs. + #this prevents pruning when kinetic model is far away from completeness + minCoreSizeForPrune=50, + #make sure that the pruned edge species have existed for a set number of RMG iterations. + #the user can specify to increase it from the default value of 2 + minSpeciesExistIterationsForPrune=2, + #filter the reactions during the enlarge step to omit species from reacting if their + #concentration are deemed to be too low + filterReactions=True, + maxNumSpecies=24, +) + +options( + #provides a name for the seed mechanism produced at the end of an rmg run default is 'Seed' + name='Syngas', + #if True every iteration it saves the current model as libraries/seeds + #(and deletes the old one) + #Unlike HTML this is inexpensive time-wise + #note a seed mechanism will be generated at the end of a completed run and some incomplete + #runs even if this is set as False + generateSeedEachIteration=True, + #If True the mechanism will also be saved directly as kinetics and thermo libraries in the database + saveSeedToDatabase=False, + #only option is 'si' + units='si', + #how often you want to save restart files. + #takes significant amount of time. comment out if you don't want to save + saveRestartPeriod=None, + #Draws images of species and reactions and saves the model output to HTML. + #May consume extra memory when running large models. + generateOutputHTML=True, + #generates plots of the RMG's performance statistics. Not helpful if you just want a model. + generatePlots=False, + #saves mole fraction of species in 'solver/' to help you create plots + saveSimulationProfiles=True, + #gets RMG to output comments on where kinetics were obtained in the chemkin file. + #useful for debugging kinetics but increases memory usage of the chemkin output file + verboseComments=False, + #gets RMG to generate edge species chemkin files. Uses lots of memory in output. + #Helpful for seeing why some reaction are not appearing in core model. + saveEdgeSpecies=False, + #Sets a time limit in the form DD:HH:MM:SS after which the RMG job will stop. Useful for profiling on jobs that + #do not converge. + #wallTime = '00:00:00', + keepIrreversible=False, + #Forces RMG to import library reactions as reversible (default). Otherwise, if set to True, RMG will import library + #reactions while keeping the reversibility as as. +) + +# optional module allows for correction to unimolecular reaction rates at low pressures and/or temperatures. +pressureDependence( + #two methods available: 'modified strong collision' is faster and less accurate than 'reservoir state' + method='modified strong collision', + #these two categories determine how fine energy is descretized. + #more grains increases accuracy but takes longer + maximumGrainSize=(0.5,'kcal/mol'), + minimumNumberOfGrains=250, + #the conditions for the rate to be output over + #parameter order is: low_value, high_value, units, internal points + temperatures=(300,2200,'K',2), + pressures=(0.01,100.01,'bar',3), + #The two options for interpolation are 'PDepArrhenius' (no extra arguments) and + #'Chebyshev' which is followed by the number of basis sets in + #Temperature and Pressure. These values must be less than the number of + #internal points specified above + interpolation=('Chebyshev', 6, 4), + #turns off pressure dependence for molecules with number of atoms greater than the number specified below + #this is due to faster internal rate of energy transfer for larger molecules + maximumAtoms=15, + ) + +#optional block adds constraints on what RMG can output. +#This is helpful for improving the efficiency of RMG, but wrong inputs can lead to many errors. +generatedSpeciesConstraints( + #allows exceptions to the following restrictions + allowed=['input species','seed mechanisms','reaction libraries'], + #maximum number of each atom in a molecule + maximumCarbonAtoms=4, + maximumOxygenAtoms=6, + maximumNitrogenAtoms=0, + maximumSiliconAtoms=0, + maximumSulfurAtoms=0, + #max number of non-hydrogen atoms + #maximumHeavyAtoms=20, + #maximum radicals on a molecule + maximumRadicalElectrons=2, + #If this is false or missing, RMG will throw an error if the more less-stable form of O2 is entered + #which doesn't react in the RMG system. normally input O2 as triplet with SMILES [O][O] + #allowSingletO2=False, + # maximum allowed number of non-normal isotope atoms: + #maximumIsotopicAtoms=2, +) + diff --git a/examples/rmg/ch3no2/input.py b/examples/rmg/ch3no2/input.py index 88153b4155..3130b18d5c 100644 --- a/examples/rmg/ch3no2/input.py +++ b/examples/rmg/ch3no2/input.py @@ -79,6 +79,7 @@ maximumEdgeSpecies=10000 ) +simulator(atol=1e-16,rtol=1e-8) options( units='si', saveRestartPeriod=None, diff --git a/examples/rmg/minimal_surface/input.py b/examples/rmg/minimal_surface/input.py index e40d88d34d..b470c15f90 100644 --- a/examples/rmg/minimal_surface/input.py +++ b/examples/rmg/minimal_surface/input.py @@ -55,6 +55,7 @@ toleranceMoveEdgeReactionToSurface=1.0, toleranceMoveSurfaceReactionToCore=2.0, toleranceMoveSurfaceSpeciesToCore=.001, + dynamicsTimeScale=(1.0e-10,'sec'), ) options( diff --git a/meta.yaml b/meta.yaml index 6098d96a32..45fcae9b49 100644 --- a/meta.yaml +++ b/meta.yaml @@ -75,7 +75,7 @@ requirements: - pyzmq - quantities - rdkit >=2015.09.2 - - rmgdatabase >=2.1.5 + - rmgdatabase >=2.1.6 - scipy - scoop - symmetry diff --git a/rmgpy/cantherm/kinetics.py b/rmgpy/cantherm/kinetics.py index 43c0677991..d1281d7411 100644 --- a/rmgpy/cantherm/kinetics.py +++ b/rmgpy/cantherm/kinetics.py @@ -185,7 +185,7 @@ def save(self, outputFile): f.write('# ======= =========== =========== =========== ===============\n') if self.Tlist is None: - Tlist = [300,400,500,600,800,1000,1500,2000] + Tlist = numpy.array([300,400,500,600,800,1000,1500,2000]) else: Tlist =self.Tlist.value_si diff --git a/rmgpy/cantherm/molepro.py b/rmgpy/cantherm/molepro.py index 8d425f5be3..a2673dcd6a 100644 --- a/rmgpy/cantherm/molepro.py +++ b/rmgpy/cantherm/molepro.py @@ -66,14 +66,17 @@ def loadEnergy(self,frequencyScaleFactor=1.): E0=None if f12a: while line!='': - #first one is for radicals second is for non radicals - if 'RHF-UCCSD(T)-F12a energy' in line or 'CCSD(T)-F12a total energy ' in line: + if ('RHF-UCCSD(T)-F12a energy' in line + or 'RHF-RCCSD(T)-F12a energy' in line + or 'CCSD(T)-F12a total energy ' in line): E0=float(line.split()[-1]) break line=f.readline() else: while line!='': - if 'RHF-UCCSD(T)-F12b energy' in line or 'CCSD(T)-F12b total energy ' in line: + if ('RHF-UCCSD(T)-F12b energy' in line + or 'RHF-RCCSD(T)-F12b energy' in line + or 'CCSD(T)-F12b total energy ' in line): E0=float(line.split()[-1]) break line=f.readline() diff --git a/rmgpy/chemkin.py b/rmgpy/chemkin.py index d3804579be..9386c188b1 100644 --- a/rmgpy/chemkin.py +++ b/rmgpy/chemkin.py @@ -535,6 +535,27 @@ def _readKineticsLine(line, reaction, speciesDict, Eunits, kunits, klow_units, k raise ChemkinError(error_msg) return kinetics +def _removeLineBreaks(comments): + """ + This method removes any extra line breaks in reaction comments, so they + can be parsed by readReactionComments. + """ + comments = comments.replace('\n',' ') + new_statement_indicators = ['Reaction index','Template reaction','Library reaction', + 'PDep reaction','Flux pairs', + 'Estimated using','Exact match found','Average of ', + 'Euclidian distance','Matched node ','Matched reaction ', + 'Multiplied by reaction path degeneracy ', + 'Kinetics were estimated in this direction', + 'dGrxn(298 K) = ','Both directions are estimates', + 'Other direction matched ','Both directions matched ', + 'This direction matched an entry in ', 'From training reaction', + 'This reaction matched rate rule','family: ','Warning:', + 'Chemkin file stated explicit reverse rate:','Ea raised from', + ] + for indicator in new_statement_indicators: + comments = comments.replace(' ' + indicator, '\n' + indicator,1) + return comments def readReactionComments(reaction, comments, read = True): """ @@ -561,6 +582,9 @@ def readReactionComments(reaction, comments, read = True): return reaction + # the comments could have line breaks that will mess up reading + # we will now combine the lines and separate them based on statements + comments = _removeLineBreaks(comments) lines = comments.strip().splitlines() for line in lines: @@ -628,6 +652,23 @@ def readReactionComments(reaction, comments, read = True): reaction.pairs.append((reactant, product)) assert len(reaction.pairs) == max(len(reaction.reactants), len(reaction.products)) + elif isinstance(reaction,TemplateReaction) and 'rate rule ' in line: + bracketed_rule = tokens[-1] + templates = bracketed_rule[1:-1].split(';') + reaction.template = templates + # still add kinetic comment + reaction.kinetics.comment += line.strip() + "\n" + + elif isinstance(reaction,TemplateReaction) and\ + 'Multiplied by reaction path degeneracy ' in line: + degen = float(tokens[-1]) + reaction.degeneracy = degen + # undo the kinetic manipulation caused by setting degneracy + if reaction.kinetics: + reaction.kinetics.changeRate(1./degen) + # still add kinetic comment + reaction.kinetics.comment += line.strip() + "\n" + elif line.strip() != '': # Any lines which are commented out but don't have any specific flag are simply kinetics comments reaction.kinetics.comment += line.strip() + "\n" @@ -714,7 +755,7 @@ def loadSpeciesDictionary(path): if line.strip() == '' and adjlist.strip() != '': # Finish this adjacency list species = Species().fromAdjacencyList(adjlist) - species.generateResonanceIsomers() + species.generate_resonance_structures() label = species.label for inert in inerts: if inert.isIsomorphic(species): @@ -732,7 +773,7 @@ def loadSpeciesDictionary(path): else: #reach end of file if adjlist.strip() != '': species = Species().fromAdjacencyList(adjlist) - species.generateResonanceIsomers() + species.generate_resonance_structures() label = species.label for inert in inerts: if inert.isIsomorphic(species): diff --git a/rmgpy/chemkinTest.py b/rmgpy/chemkinTest.py index 046c27ac89..a88cf4d06a 100644 --- a/rmgpy/chemkinTest.py +++ b/rmgpy/chemkinTest.py @@ -29,8 +29,11 @@ import mock import os from chemkin import * +from chemkin import _removeLineBreaks import rmgpy from rmgpy.species import Species +from rmgpy.reaction import Reaction +from rmgpy.kinetics.arrhenius import Arrhenius ################################################### @@ -116,9 +119,9 @@ def test_readThermoEntry_NoTRange(self): self.assertEqual(formula, {'H': 6, 'C': 2}) self.assertTrue(isinstance(thermo, NASA)) - def testReadAndWriteTemplateReactionFamilyForMinimalExample(self): + def testReadAndWriteAndReadTemplateReactionFamilyForMinimalExample(self): """ - This example is mainly to test if family info can be correctly + This example tests if family and templates info can be correctly parsed from comments like '!Template reaction: R_Recombination'. """ folder = os.path.join(os.path.dirname(rmgpy.__file__), 'test_data/chemkin/chemkin_py') @@ -126,15 +129,16 @@ def testReadAndWriteTemplateReactionFamilyForMinimalExample(self): chemkinPath = os.path.join(folder, 'minimal', 'chem.inp') dictionaryPath = os.path.join(folder, 'minimal', 'species_dictionary.txt') - # loadChemkinFile + # read original chemkin file species, reactions = loadChemkinFile(chemkinPath, dictionaryPath) + # ensure correct reading reaction1 = reactions[0] self.assertEqual(reaction1.family, "R_Recombination") - + self.assertEqual(frozenset('C_methyl;C_methyl'.split(';')), frozenset(reaction1.template)) reaction2 = reactions[1] self.assertEqual(reaction2.family, "H_Abstraction") - + self.assertEqual(frozenset('C/H3/Cs\H3;C_methyl'.split(';')), frozenset(reaction2.template)) # saveChemkinFile chemkinSavePath = os.path.join(folder, 'minimal', 'chem_new.inp') dictionarySavePath = os.path.join(folder, 'minimal', 'species_dictionary_new.txt') @@ -145,6 +149,19 @@ def testReadAndWriteTemplateReactionFamilyForMinimalExample(self): self.assertTrue(os.path.isfile(chemkinSavePath)) self.assertTrue(os.path.isfile(dictionarySavePath)) + # read newly written chemkin file to make sure the entire cycle works + _, reactions2 =loadChemkinFile(chemkinSavePath, dictionarySavePath) + + reaction1_new = reactions2[0] + self.assertEqual(reaction1_new.family, reaction1_new.family) + self.assertEqual(reaction1_new.template, reaction1_new.template) + self.assertEqual(reaction1_new.degeneracy, reaction1_new.degeneracy) + + reaction2_new = reactions2[1] + self.assertEqual(reaction2_new.family, reaction2_new.family) + self.assertEqual(reaction2_new.template, reaction2_new.template) + self.assertEqual(reaction2_new.degeneracy, reaction2_new.degeneracy) + # clean up os.remove(chemkinSavePath) os.remove(dictionarySavePath) @@ -290,3 +307,168 @@ def testReadSpecificCollider(self): self.assertEqual(reaction.specificCollider.label, 'N2(5)') +class TestReadReactionComments(unittest.TestCase): + @classmethod + def setUpClass(self): + r = Species().fromSMILES('[CH3]') + r.label = '[CH3]' + p = Species().fromSMILES('CC') + p.label = 'CC' + + self.reaction = Reaction(reactants=[r,r], + products=[p], + kinetics = Arrhenius(A=(8.26e+17,'cm^3/(mol*s)'), + n=-1.4, + Ea=(1,'kcal/mol'), T0=(1,'K')) + ) + self.comments_list = [""" +Reaction index: Chemkin #1; RMG #1 +Template reaction: R_Recombination +Exact match found for rate rule (C_methyl;C_methyl) +Multiplied by reaction path degeneracy 0.5 +""", +""" +Reaction index: Chemkin #2; RMG #4 +Template reaction: H_Abstraction +Estimated using template (C/H3/Cs;C_methyl) for rate rule (C/H3/Cs\H3;C_methyl) +Multiplied by reaction path degeneracy 6 +""", +""" +Reaction index: Chemkin #13; RMG #8 +Template reaction: H_Abstraction +Flux pairs: [CH3], CC; [CH3], CC; +Estimated using an average for rate rule [C/H3/Cs\H3;C_rad/H2/Cs] +Multiplied by reaction path degeneracy 6.0 +""", +""" +Reaction index: Chemkin #17; RMG #31 +Template reaction: H_Abstraction +Flux pairs: [CH3], CC; [CH3], CC; +Estimated using average of templates [C/H3/Cs;H_rad] + [C/H3/Cs\H3;Y_rad] for rate rule [C/H3/Cs\H3;H_rad] +Multiplied by reaction path degeneracy 6.0 +""", +""" +Reaction index: Chemkin #69; RMG #171 +Template reaction: intra_H_migration +Flux pairs: [CH3], CC; [CH3], CC; +Estimated using average of templates [R3H_SS;O_rad_out;Cs_H_out_2H] + [R3H_SS_Cs;Y_rad_out;Cs_H_out_2H] for rate rule +[R3H_SS_Cs;O_rad_out;Cs_H_out_2H] +Multiplied by reaction path degeneracy 3.0 +""", +""" +Reaction index: Chemkin #3; RMG #243 +Template reaction: Disproportionation +Flux pairs: [CH3], CC; [CH3], CC; +Average of [Average of [O2b;O_Csrad] + Average of [O_atom_triplet;O_Csrad + CH2_triplet;O_Csrad] + Average of [Average of [Ct_rad/Ct;O_Csrad from +training reaction 0] + Average of [O_pri_rad;O_Csrad + Average of [O_rad/NonDeC;O_Csrad + O_rad/NonDeO;O_Csrad]] + Average of [Cd_pri_rad;O_Csrad] + +Average of [CO_pri_rad;O_Csrad] + Average of [C_methyl;O_Csrad + Average of [C_rad/H2/Cs;O_Csrad + C_rad/H2/Cd;O_Csrad + C_rad/H2/O;O_Csrad] + Average +of [C_rad/H/NonDeC;O_Csrad] + Average of [Average of [C_rad/Cs3;O_Csrad]]] + H_rad;O_Csrad]] +Estimated using template [Y_rad_birad_trirad_quadrad;O_Csrad] for rate rule [CH_quartet;O_Csrad] +""", +""" +Reaction index: Chemkin #4; RMG #303 +Template reaction: Disproportionation +Flux pairs: [CH3], CC; [CH3], CC; +Matched reaction 0 C2H + CH3O <=> C2H2 + CH2O in Disproportionation/training +""", +""" +Reaction index: Chemkin #51; RMG #136 +Template reaction: H_Abstraction +Flux pairs: [CH3], CC; [CH3], CC; +Estimated using an average for rate rule [C/H3/Cd\H_Cd\H2;C_rad/H2/Cs] +Euclidian distance = 0 +Multiplied by reaction path degeneracy 3.0 +""", +""" +Reaction index: Chemkin #32; RMG #27 +Template reaction: R_Recombination +Flux pairs: [CH3], CC; [CH3], CC; +Matched reaction 20 CH3 + CH3 <=> C2H6 in R_Recombination/training +This reaction matched rate rule [C_methyl;C_methyl] +""", +""" +Reaction index: Chemkin #2; RMG #4 +Template reaction: R_Recombination +Flux pairs: [CH3], CC; [CH3], CC; +From training reaction 21 for rate rule [C_rad/H2/Cs;C_methyl] +Exact match found for rate rule [C_rad/H2/Cs;C_methyl] +Euclidian distance = 0 +"""] + self.template_list = [['C_methyl','C_methyl'], + ['C/H3/Cs\H3','C_methyl'], + ['C/H3/Cs\H3','C_rad/H2/Cs'], + ['C/H3/Cs\H3','H_rad'], + ['R3H_SS_Cs','O_rad_out','Cs_H_out_2H'], + ['CH_quartet','O_Csrad'], + None, + ['C/H3/Cd\H_Cd\H2','C_rad/H2/Cs'], + ['C_methyl','C_methyl'], + ['C_rad/H2/Cs','C_methyl']] + self.family_list = ['R_Recombination', + 'H_Abstraction', + 'H_Abstraction', + 'H_Abstraction', + 'intra_H_migration', + 'Disproportionation', + 'Disproportionation', + 'H_Abstraction', + 'R_Recombination', + 'R_Recombination',] + self.degeneracy_list = [0.5, + 6, + 6, + 6, + 3, + 1, + 1, + 3, + 1, + 1] + self.expected_lines = [4,4,5,5,5,5,4,6,5,6] + + def testReadReactionCommentsTemplate(self): + """ + Test that the template is picked up from reading reaction comments. + """ + for index, comment in enumerate(self.comments_list): + new_rxn = readReactionComments(self.reaction, comment) + + # only check template if meant to find one + if self.template_list[index]: + self.assertTrue(new_rxn.template,'The template was not saved from the reaction comment {}'.format(comment)) + self.assertEqual(frozenset(new_rxn.template),frozenset(self.template_list[index]),'The reaction template does not match') + else: + self.assertFalse(new_rxn.template) + + def testReadReactionCommentsFamily(self): + """ + Test that the family is picked up from reading reaction comments. + """ + for index, comment in enumerate(self.comments_list): + new_rxn = readReactionComments(self.reaction, comment) + + self.assertEqual(new_rxn.family, self.family_list[index], 'wrong reaction family stored') + + def testReadReactionCommentsDegeneracy(self): + """ + Test that the degeneracy is picked up from reading reaction comments. + + Also checks that reaction rate was not modified in the process. + """ + for index, comment in enumerate(self.comments_list): + previous_rate = self.reaction.kinetics.A.value_si + new_rxn = readReactionComments(self.reaction, comment) + new_rate = new_rxn.kinetics.A.value_si + + self.assertEqual(new_rxn.degeneracy, self.degeneracy_list[index], 'wrong degeneracy was stored') + self.assertEqual(previous_rate, new_rate) + + def testRemoveLineBreaks(self): + """ + tests that _removeLineBreaks functions properly + """ + for index, comment in enumerate(self.comments_list): + new_comment = _removeLineBreaks(comment) + new_comment_lines = len(new_comment.strip().splitlines()) + self.assertEqual(new_comment_lines, self.expected_lines[index], + 'Found {} more lines than expected for comment \n\n""{}""\n\n which converted to \n\n""{}""'.format(new_comment_lines - self.expected_lines[index],comment.strip(), new_comment.strip())) diff --git a/rmgpy/data/base.py b/rmgpy/data/base.py index 2706512b0c..79a1502680 100644 --- a/rmgpy/data/base.py +++ b/rmgpy/data/base.py @@ -282,7 +282,7 @@ def getSpecies(self, path): if line.strip() == '' and adjlist.strip() != '': # Finish this adjacency list species = Species().fromAdjacencyList(adjlist) - species.generateResonanceIsomers() + species.generate_resonance_structures() label = species.label if label in speciesDict: raise DatabaseError('Species label "{0}" used for multiple species in {1}.'.format(label, str(self))) @@ -294,7 +294,7 @@ def getSpecies(self, path): if adjlist.strip() != '': # Finish this adjacency list species = Species().fromAdjacencyList(adjlist) - species.generateResonanceIsomers() + species.generate_resonance_structures() label = species.label if label in speciesDict: raise DatabaseError('Species label "{0}" used for multiple species in {1}.'.format(label, str(self))) diff --git a/rmgpy/data/kinetics/common.py b/rmgpy/data/kinetics/common.py index b3513b93a7..5ee6e9dc3e 100644 --- a/rmgpy/data/kinetics/common.py +++ b/rmgpy/data/kinetics/common.py @@ -32,10 +32,13 @@ This module contains classes and functions that are used by multiple modules in this subpackage. """ +import itertools +import logging +import warnings from rmgpy.data.base import LogicNode from rmgpy.reaction import Reaction -from rmgpy.molecule import Group +from rmgpy.molecule import Group, Molecule from rmgpy.species import Species from rmgpy.exceptions import DatabaseError, KineticsError @@ -141,7 +144,7 @@ def sortEfficiencies(efficiencies0): f.write(')\n\n') -def filterReactions(reactants, products, reactionList): +def filter_reactions(reactants, products, reactionList): """ Remove any reactions from the given `reactionList` whose reactants do not involve all the given `reactants` or whose products do not involve @@ -150,27 +153,12 @@ def filterReactions(reactants, products, reactionList): reactants and products can be either molecule or species objects """ + warnings.warn("The filter_reactions method is no longer used and may be removed in a future version.", DeprecationWarning) # Convert from molecules to species and generate resonance isomers. - reactant_species = [] - for mol in reactants: - if isinstance(mol,Species): - s = mol - else: - s = Species(molecule=[mol]) - s.generateResonanceIsomers() - reactant_species.append(s) - reactants = reactant_species - product_species = [] - for mol in products: - if isinstance(mol,Species): - s = mol - else: - s = Species(molecule=[mol]) - s.generateResonanceIsomers() - product_species.append(s) - products = product_species - + reactants = ensure_species(reactants, resonance=True) + products = ensure_species(products, resonance=True) + reactions = reactionList[:] for reaction in reactionList: @@ -205,3 +193,211 @@ def filterReactions(reactants, products, reactionList): if not forward and not reverse: reactions.remove(reaction) return reactions + + +def ensure_species(input_list, resonance=False, keepIsomorphic=False): + """ + Given an input list of molecules or species, return a list with only + species objects. + """ + output_list = [] + for item in input_list: + if isinstance(item, Molecule): + new_item = Species(molecule=[item]) + elif isinstance(item, Species): + new_item = item + else: + raise TypeError('Only Molecule or Species objects can be handled.') + if resonance: + new_item.generate_resonance_structures(keepIsomorphic=keepIsomorphic) + output_list.append(new_item) + + return output_list + + +def generate_molecule_combos(input_species): + """ + Generate combinations of molecules from the given species objects. + """ + if len(input_species) == 1: + combos = [(mol,) for mol in input_species[0].molecule] + elif len(input_species) == 2: + combos = itertools.product(input_species[0].molecule, input_species[1].molecule) + else: + raise ValueError('Reaction generation can be done for 1 or 2 species, not {0}.'.format(len(input_species))) + + return combos + + +def ensure_independent_atom_ids(input_species, resonance=True): + """ + Given a list or tuple of :class:`Species` objects, ensure that atom ids are + independent across all of the species. Optionally, the `resonance` argument + can be set to False to not generate resonance structures. + + Modifies the input species in place, nothing is returned. + """ + + # Method to check that all species' atom ids are different + def independent_ids(): + num_atoms = 0 + IDs = [] + for species in input_species: + num_atoms += len(species.molecule[0].atoms) + IDs.extend([atom.id for atom in species.molecule[0].atoms]) + num_ID = len(set(IDs)) + return num_ID == num_atoms + + # If they are not all different, reassign ids and remake resonance structures + if not independent_ids(): + logging.debug('identical atom ids found between species. regenerating') + for species in input_species: + mol = species.molecule[0] + mol.assignAtomIDs() + species.molecule = [mol] + # Remake resonance structures with new labels + if resonance: + species.generate_resonance_structures(keepIsomorphic=True) + elif resonance: + # IDs are already independent, generate resonance structures if needed + for species in input_species: + species.generate_resonance_structures(keepIsomorphic=True) + + +def find_degenerate_reactions(rxnList, same_reactants=None, kinetics_database=None, kinetics_family=None): + """ + given a list of Reaction object with Molecule objects, this method + removes degenerate reactions and increments the degeneracy of the + reaction object. For multiple transition states, this method adds + them as separate duplicate reactions. This method modifies + rxnList in place and does not return anything. + + This algorithm used to exist in family.__generateReactions, but was moved + here because it didn't have any family dependence. + """ + + # We want to sort all the reactions into sublists composed of isomorphic reactions + # with degenerate transition states + rxnSorted = [] + for rxn0 in rxnList: + # find resonance structures for rxn0 + ensure_species_in_reaction(rxn0) + if len(rxnSorted) == 0: + # This is the first reaction, so create a new sublist + rxnSorted.append([rxn0]) + else: + # Loop through each sublist, which represents a unique reaction + for rxnList1 in rxnSorted: + # Try to determine if the current rxn0 is identical or isomorphic to any reactions in the sublist + isomorphic = False + identical = False + sameTemplate = False + for rxn in rxnList1: + isomorphic = rxn0.isIsomorphic(rxn, checkIdentical=False, checkTemplateRxnProducts=True) + if not isomorphic: + identical = False + else: + identical = rxn0.isIsomorphic(rxn, checkIdentical=True, checkTemplateRxnProducts=True) + sameTemplate = frozenset(rxn.template) == frozenset(rxn0.template) + if not isomorphic: + # a different product was found, go to next list + break + elif not sameTemplate: + # a different transition state was found, mark as duplicate and + # go to the next sublist + rxn.duplicate = True + rxn0.duplicate = True + break + elif identical: + # An exact copy of rxn0 is already in our list, so we can move on to the next rxn + break + else: # sameTemplate and isomorphic but not identical + # This is the right sublist for rxn0, but continue to see if there is an identical rxn + continue + else: + # We did not break, so this is the right sublist, but there is no identical reaction + # This means that we should add rxn0 to the sublist as a degenerate rxn + rxnList1.append(rxn0) + if isomorphic and sameTemplate: + # We already found the right sublist, so we can move on to the next rxn + break + else: + # We did not break, which means that there was no isomorphic sublist, so create a new one + rxnSorted.append([rxn0]) + + rxnList = [] + for rxnList1 in rxnSorted: + # Collapse our sorted reaction list by taking one reaction from each sublist + rxn = rxnList1[0] + # The degeneracy of each reaction is the number of reactions that were in the sublist + rxn.degeneracy = sum([reaction0.degeneracy for reaction0 in rxnList1]) + rxnList.append(rxn) + + for rxn in rxnList: + if rxn.isForward: + reduce_same_reactant_degeneracy(rxn, same_reactants) + else: + # fix the degeneracy of (not ownReverse) reactions found in the backwards direction + try: + family = kinetics_family or kinetics_database.families[rxn.family] + except AttributeError: + from rmgpy.data.rmg import getDB + family = getDB('kinetics').families[rxn.family] + if not family.ownReverse: + rxn.degeneracy = family.calculateDegeneracy(rxn) + + return rxnList + + +def ensure_species_in_reaction(reaction): + """ + Modifies a reaction holding Molecule objects to a reaction holding + Species objects. Generates resonance structures for reaction products. + """ + # if already species' objects, return none + if isinstance(reaction.reactants[0], Species): + return None + # obtain species with all resonance isomers + if reaction.isForward: + reaction.reactants = ensure_species(reaction.reactants, resonance=False) + reaction.products = ensure_species(reaction.products, resonance=True, keepIsomorphic=True) + else: + reaction.reactants = ensure_species(reaction.reactants, resonance=True, keepIsomorphic=True) + reaction.products = ensure_species(reaction.products, resonance=False) + + # convert reaction.pairs object to species + new_pairs = [] + for reactant, product in reaction.pairs: + new_pair = [] + for reactant0 in reaction.reactants: + if reactant0.isIsomorphic(reactant): + new_pair.append(reactant0) + break + for product0 in reaction.products: + if product0.isIsomorphic(product): + new_pair.append(product0) + break + new_pairs.append(new_pair) + reaction.pairs = new_pairs + + try: + ensure_species_in_reaction(reaction.reverse) + except AttributeError: + pass + + +def reduce_same_reactant_degeneracy(reaction, same_reactants=None): + """ + This method reduces the degeneracy of reactions with identical reactants, + since translational component of the transition states are already taken + into account (so swapping the same reactant is not valid) + + This comes from work by Bishop and Laidler in 1965 + """ + if len(reaction.reactants) == 2 and ( + (reaction.isForward and same_reactants) or + reaction.reactants[0].isIsomorphic(reaction.reactants[1]) + ): + reaction.degeneracy *= 0.5 + logging.debug('Degeneracy of reaction {} was decreased by 50% to {} since the reactants are identical'.format(reaction, reaction.degeneracy)) + diff --git a/rmgpy/data/kinetics/database.py b/rmgpy/data/kinetics/database.py index 7c3ee75251..5c789a82e0 100644 --- a/rmgpy/data/kinetics/database.py +++ b/rmgpy/data/kinetics/database.py @@ -45,7 +45,8 @@ from .family import KineticsFamily from .library import LibraryReaction, KineticsLibrary -from .common import filterReactions +from .common import ensure_species, generate_molecule_combos, \ + find_degenerate_reactions, ensure_independent_atom_ids from rmgpy.exceptions import DatabaseError ################################################################################ @@ -356,39 +357,42 @@ def saveOld(self, path): onoff = 'on ' if self.recommendedFamilies[label] else 'off' f.write("{num:<2d} {onoff} {label}\n".format(num=number, label=label, onoff=onoff)) - def generateReactions(self, reactants, products=None): + def generate_reactions(self, reactants, products=None, only_families=None, resonance=True): """ Generate all reactions between the provided list of one or two `reactants`, which should be :class:`Molecule` objects. This method searches the depository, libraries, and groups, in that order. """ reactionList = [] - reactionList.extend(self.generateReactionsFromLibraries(reactants, products)) - reactionList.extend(self.generateReactionsFromFamilies(reactants, products)) + if only_families is None: + reactionList.extend(self.generate_reactions_from_libraries(reactants, products)) + reactionList.extend(self.generate_reactions_from_families(reactants, products, only_families=None, resonance=True)) return reactionList - def generateReactionsFromLibraries(self, reactants, products): + def generate_reactions_from_libraries(self, reactants, products=None): """ - Generate all reactions between the provided list of one or two - `reactants`, which should be :class:`Molecule` objects. This method - searches the depository. + Find all reactions from all loaded kinetics library involving the + provided `reactants`, which can be either :class:`Molecule` objects or + :class:`Species` objects. """ - reactionList = [] - for label, libraryType in self.libraryOrder: + reaction_list = [] + for label, library_type in self.libraryOrder: # Generate reactions from reaction libraries (no need to generate them from seeds) - if libraryType == "Reaction Library": - reactionList.extend(self.generateReactionsFromLibrary(reactants, products, self.libraries[label])) - return reactionList + if library_type == "Reaction Library": + reaction_list.extend(self.generate_reactions_from_library(self.libraries[label], reactants, products=products)) + return reaction_list - def generateReactionsFromLibrary(self, reactants, products, library): + def generate_reactions_from_library(self, library, reactants, products=None): """ - Generate all reactions between the provided list of one or two - `reactants`, which should be :class:`Molecule` objects. This method - searches the depository. + Find all reactions from the specified kinetics library involving the + provided `reactants`, which can be either :class:`Molecule` objects or + :class:`Species` objects. """ - reactionList = [] + reactants = ensure_species(reactants) + + reaction_list = [] for entry in library.entries.values(): - if entry.item.matchesMolecules(reactants): + if entry.item.matchesSpecies(reactants, products=products): reaction = LibraryReaction( reactants = entry.item.reactants[:], products = entry.item.products[:], @@ -400,37 +404,79 @@ def generateReactionsFromLibrary(self, reactants, products, library): library = library, entry = entry, ) - reactionList.append(reaction) - if products: - reactionList = filterReactions(reactants, products, reactionList) - return reactionList - - def generateReactionsFromFamilies(self, reactants, products, only_families=None): - """ - Generate all reactions between the provided list of one or two - `reactants`, which should be :class:`Molecule` objects. This method - applies the reaction family. - If `only_families` is a list of strings, only families with those labels - are used. - """ - # If there are two structures and they are the same, then make a copy - # of the second one so we can independently manipulate both of them - # This is for the case where A + A --> products - if len(reactants) == 2 and reactants[0] == reactants[1]: - reactants[1] = reactants[1].copy(deep=True) - - reactionList = [] + reaction_list.append(reaction) + + return reaction_list + + def generate_reactions_from_families(self, reactants, products=None, only_families=None, resonance=True): + """ + Generate all reactions between the provided list or tuple of one or two + `reactants`, which can be either :class:`Molecule` objects or :class:`Species` + objects. This method can apply all kinetics families or a selected subset. + + Args: + reactants: Molecules or Species to react + products: List of Molecules or Species of desired product structures (optional) + only_families: List of family labels to generate reactions from (optional) + Default is to generate reactions from all families + resonance: Flag to generate resonance structures for reactants and products (optional) + Default is True, resonance structures will be generated + + Returns: + List of reactions containing Species objects with the specified reactants and products. + """ + # 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 + same_reactants = False + if len(reactants) == 2: + if reactants[0] is reactants[1]: + reactants[1] = reactants[1].copy(deep=True) + same_reactants = True + elif reactants[0].isIsomorphic(reactants[1]): + same_reactants = True + + # Convert to Species objects if necessary + reactants = ensure_species(reactants) + + # Label reactant atoms for proper degeneracy calculation + ensure_independent_atom_ids(reactants, resonance=resonance) + + combos = generate_molecule_combos(reactants) + + reaction_list = [] + for combo in combos: + reaction_list.extend(self.react_molecules(combo, products=products, only_families=only_families, prod_resonance=resonance)) + + # Calculate reaction degeneracy + reaction_list = find_degenerate_reactions(reaction_list, same_reactants, kinetics_database=self) + # Add reverse attribute to families with ownReverse + to_delete = [] + for i, rxn in enumerate(reaction_list): + family = self.families[rxn.family] + if family.ownReverse: + successful = family.addReverseAttribute(rxn) + if not successful: + to_delete.append(i) + # Delete reactions which we could not find a reverse reaction for + for i in reversed(to_delete): + del reaction_list[i] + + return reaction_list + + def react_molecules(self, molecules, products=None, only_families=None, prod_resonance=True): + """ + Generate reactions from all families for the input molecules. + """ + reaction_list = [] for label, family in self.families.iteritems(): if only_families is None or label in only_families: - try: - reactionList.extend(family.generateReactions(reactants)) - except: - logging.error("Problem family: {}".format(label)) - logging.error("Problem reactants: {}".format(reactants)) - raise - if products: - reactionList = filterReactions(reactants, products, reactionList) - return reactionList + reaction_list.extend(family.generateReactions(molecules, products=products, prod_resonance=prod_resonance)) + + for reactant in molecules: + reactant.clearLabeledAtoms() + + return reaction_list def getForwardReactionForFamilyEntry(self, entry, family, thermoDatabase): """ @@ -491,17 +537,17 @@ def matchSpeciesToMolecules(species, molecules): reaction = Reaction(reactants=[], products=[]) for molecule in entry.item.reactants: reactant = Species(molecule=[molecule]) - reactant.generateResonanceIsomers() + reactant.generate_resonance_structures() reactant.thermo = thermoDatabase.getThermoData(reactant) reaction.reactants.append(reactant) for molecule in entry.item.products: product = Species(molecule=[molecule]) - product.generateResonanceIsomers() + product.generate_resonance_structures() product.thermo = thermoDatabase.getThermoData(product) reaction.products.append(product) # Generate all possible reactions involving the reactant species - generatedReactions = self.generateReactionsFromFamilies([reactant.molecule for reactant in reaction.reactants], [], only_families=[family]) + generatedReactions = self.generate_reactions_from_families([reactant.molecule for reactant in reaction.reactants], [], only_families=[family]) # Remove from that set any reactions that don't produce the desired reactants and products forward = []; reverse = [] diff --git a/rmgpy/data/kinetics/family.py b/rmgpy/data/kinetics/family.py index f0335d8d5e..a7b3b71ae1 100644 --- a/rmgpy/data/kinetics/family.py +++ b/rmgpy/data/kinetics/family.py @@ -43,9 +43,10 @@ from rmgpy.reaction import Reaction from rmgpy.kinetics import Arrhenius from rmgpy.molecule import Bond, GroupBond, Group, Molecule +from rmgpy.molecule.resonance import generate_aromatic_resonance_structures from rmgpy.species import Species -from .common import saveEntry +from .common import saveEntry, ensure_species, find_degenerate_reactions, generate_molecule_combos from .depository import KineticsDepository from .groups import KineticsGroups from .rules import KineticsRules @@ -1060,7 +1061,7 @@ def addKineticsRulesFromTrainingSet(self, thermoDatabase=None): shortDesc="Rate rule generated from training reaction {0}. ".format(entry.index) + entry.shortDesc, longDesc="Rate rule generated from training reaction {0}. ".format(entry.index) + entry.longDesc, ) - new_entry.data.comment = "{0} from training reaction {1}".format(';'.join([g.label for g in template]), entry.index) + new_entry.data.comment = "From training reaction {1} for rate rule {0}".format(';'.join([g.label for g in template]), entry.index) new_entry.data.A.value_si /= entry.item.degeneracy try: @@ -1081,10 +1082,10 @@ def addKineticsRulesFromTrainingSet(self, thermoDatabase=None): item = Reaction(reactants=[Species(molecule=[m.molecule[0].copy(deep=True)], label=m.label) for m in entry.item.reactants], products=[Species(molecule=[m.molecule[0].copy(deep=True)], label=m.label) for m in entry.item.products]) for reactant in item.reactants: - reactant.generateResonanceIsomers() + reactant.generate_resonance_structures() reactant.thermo = thermoDatabase.getThermoData(reactant, trainingSet=True) for product in item.products: - product.generateResonanceIsomers() + product.generate_resonance_structures() product.thermo = thermoDatabase.getThermoData(product,trainingSet=True) # Now that we have the thermo, we can get the reverse k(T) item.kinetics = data @@ -1108,7 +1109,7 @@ def addKineticsRulesFromTrainingSet(self, thermoDatabase=None): shortDesc="Rate rule generated from training reaction {0}. ".format(entry.index) + entry.shortDesc, longDesc="Rate rule generated from training reaction {0}. ".format(entry.index) + entry.longDesc, ) - new_entry.data.comment = "{0} from training reaction {1}".format(';'.join([g.label for g in template]), entry.index) + new_entry.data.comment = "From training reaction {1} for rate rule {0}".format(';'.join([g.label for g in template]), entry.index) new_entry.data.A.value_si /= new_degeneracy try: @@ -1435,7 +1436,7 @@ def __matchReactantToTemplate(self, reactant, templateReactant): else: raise NotImplementedError("Not expecting template of type {}".format(type(struct))) - def generateReactions(self, reactants): + def generateReactions(self, reactants, products=None, prod_resonance=True): """ Generate all reactions between the provided list of one or two `reactants`, which should be either single :class:`Molecule` objects @@ -1444,15 +1445,26 @@ def generateReactions(self, reactants): using :class:`Molecule` objects for both reactants and products The reactions are constructed such that the forward direction is consistent with the template of this reaction family. + + Args: + reactants: List of Molecules to react + products: List of Molecules or Species of desired product structures (optional) + prod_resonance: Flag to generate resonance structures for product checking (optional) + Defaults to True, resonance structures are compared + + Returns: + List of all reactions containing Molecule objects with the + specified reactants and products within this family. + Degenerate reactions are returned as separate reactions. """ reactionList = [] # Forward direction (the direction in which kinetics is defined) - reactionList.extend(self.__generateReactions(reactants, forward=True)) + reactionList.extend(self.__generateReactions(reactants, products=products, forward=True, prod_resonance=prod_resonance)) if not self.ownReverse: # Reverse direction (the direction in which kinetics is not defined) - reactionList.extend(self.__generateReactions(reactants, forward=False)) + reactionList.extend(self.__generateReactions(reactants, products=products, forward=False, prod_resonance=prod_resonance)) return reactionList @@ -1464,8 +1476,6 @@ def addReverseAttribute(self, rxn): Returns `True` if successful and `False` if the reverse reaction is forbidden. Will raise a `KineticsError` if unsuccessful for other reasons. """ - from rmgpy.rmg.react import findDegeneracies - if self.ownReverse: # Check if the reactants are the same sameReactants = False @@ -1474,7 +1484,7 @@ def addReverseAttribute(self, rxn): reactionList = self.__generateReactions([spc.molecule for spc in rxn.products], products=rxn.reactants, forward=True) - reactions = findDegeneracies(reactionList, sameReactants) + reactions = find_degenerate_reactions(reactionList, sameReactants, kinetics_family=self) if len(reactions) == 0: logging.error("Expecting one matching reverse reaction, not zero in reaction family {0} for forward reaction {1}.\n".format(self.label, str(rxn))) logging.error("There is likely a bug in the RMG-database kinetics reaction family involving a missing group, missing atomlabels, forbidden groups, etc.") @@ -1494,7 +1504,7 @@ def addReverseAttribute(self, rxn): try: reactionList = self.__generateReactions([spc.molecule for spc in rxn.products], products=rxn.reactants, forward=True) - reactions = findDegeneracies(reactionList) + reactions = find_degenerate_reactions(reactionList, sameReactants, kinetics_family=self) finally: self.forbidden = tempObject if len(reactions) == 1 or (len(reactions) > 1 and all([reactions[0].isIsomorphic(other, checkTemplateRxnProducts=True) for other in reactions])): @@ -1531,25 +1541,14 @@ def calculateDegeneracy(self, reaction): `ignoreSameReactants= True` to this method. """ reaction.degeneracy = 1 - from rmgpy.rmg.react import findDegeneracies, getMoleculeTuples # find combinations of resonance isomers - specReactants = [] - if isinstance(reaction.reactants[0], Molecule): - for mol in reaction.reactants: - spec = Species(molecule=[mol]) - spec.generateResonanceIsomers(keepIsomorphic=True) - specReactants.append(spec) - elif isinstance(reaction.reactants[0], Species): - specReactants = reaction.reactants - else: - raise TypeError('Reactants must be either Species or Molecule Objects') - molecule_combos = getMoleculeTuples(specReactants) + specReactants = ensure_species(reaction.reactants, resonance=True, keepIsomorphic=True) + molecule_combos = generate_molecule_combos(specReactants) reactions = [] for combo in molecule_combos: - comboOnlyMols = [tup[0] for tup in combo] - reactions.extend(self.__generateReactions(comboOnlyMols, products=reaction.products, forward=True)) + reactions.extend(self.__generateReactions(combo, products=reaction.products, forward=True)) # Check if the reactants are the same sameReactants = False @@ -1557,7 +1556,7 @@ def calculateDegeneracy(self, reaction): sameReactants = True # remove degenerate reactions - reactions = findDegeneracies(reactions, sameReactants) + reactions = find_degenerate_reactions(reactions, sameReactants, kinetics_family=self) # remove reactions with different templates (only for TemplateReaction) if isinstance(reaction, TemplateReaction): @@ -1581,7 +1580,7 @@ def calculateDegeneracy(self, reaction): 'but generated {2}').format(reaction, self.label, len(reactions))) return reactions[0].degeneracy - def __generateReactions(self, reactants, products=None, forward=True): + def __generateReactions(self, reactants, products=None, forward=True, prod_resonance=True): """ Generate a list of all of the possible reactions of this family between the list of `reactants`. The number of reactants provided must match @@ -1590,8 +1589,21 @@ def __generateReactions(self, reactants, products=None, forward=True): be a list of :class:`Molecule` objects, each representing a resonance isomer of the species of interest. - This method returns degenerate reactions, and `react.findDegeneracies` - can be used to find the degenerate reactions. + This method returns all reactions, and degenerate reactions can then be + found using `rmgpy.data.kinetics.common.find_degenerate_reactions`. + + Args: + reactants: List of Molecules to react + products: List of Molecules or Species of desired product structures (optional) + forward: Flag to indicate whether the forward or reverse template should be applied (optional) + Default is True, forward template is used + prod_resonance: Flag to generate resonance structures for product checking (optional) + Default is True, resonance structures are compared + + Returns: + List of all reactions containing Molecule objects with the + specified reactants and products within this family. + Degenerate reactions are returned as separate reactions. """ rxnList = []; speciesList = [] @@ -1677,39 +1689,33 @@ def __generateReactions(self, reactants, products=None, forward=True): # If products is given, remove reactions from the reaction list that # don't generate the given products if products is not None: - if isinstance(products[0],Molecule): - products = [product.generateResonanceIsomers() for product in products] - elif isinstance(products[0],Species): - for product in products: - product.generateResonanceIsomers(keepIsomorphic=False) - products = [product.molecule for product in products] - else: - raise TypeError('products input to __generateReactions must be Species or Molecule Objects') - + products = ensure_species(products, resonance=prod_resonance) + rxnList0 = rxnList[:] rxnList = [] - index = 0 for reaction in rxnList0: - products0 = reaction.products if forward else reaction.reactants - + products0 = reaction.products[:] if forward else reaction.reactants[:] + + # For aromatics, generate aromatic resonance structures to accurately identify isomorphic species + if prod_resonance: + for i, product in enumerate(products0): + if product.isCyclic: + aromaticStructs = generate_aromatic_resonance_structures(product) + if aromaticStructs: + products0[i] = aromaticStructs[0] + # Skip reactions that don't match the given products match = False if len(products) == len(products0) == 1: - for product in products[0]: - if products0[0].isIsomorphic(product): - match = True - break + if products[0].isIsomorphic(products0[0]): + match = True elif len(products) == len(products0) == 2: - for productA in products[0]: - for productB in products[1]: - if products0[0].isIsomorphic(productA) and products0[1].isIsomorphic(productB): - match = True - break - elif products0[0].isIsomorphic(productB) and products0[1].isIsomorphic(productA): - match = True - break + if products[0].isIsomorphic(products0[0]) and products[1].isIsomorphic(products0[1]): + match = True + elif products[0].isIsomorphic(products0[1]) and products[1].isIsomorphic(products0[0]): + match = True elif len(products) == len(products0): raise NotImplementedError("Can't yet filter reactions with {} products".format(len(products))) @@ -2136,12 +2142,11 @@ def retrieveOriginalEntry(self, templateLabel): Where the TrainingReactionEntry is only present if it comes from a training reaction """ - - templateLabels = templateLabel.split()[0].split(';') + templateLabels = templateLabel.split()[-1].split(';') template = self.retrieveTemplate(templateLabels) rule = self.getRateRule(template) - if 'from training reaction' in rule.data.comment: - trainingIndex = int(rule.data.comment.split()[-1]) + if 'From training reaction' in rule.data.comment: + trainingIndex = int(rule.data.comment.split()[3]) trainingDepository = self.getTrainingDepository() return rule, trainingDepository.entries[trainingIndex] else: @@ -2193,7 +2198,7 @@ def assignWeightsToEntries(entryNestedList, weightedEntries, N = 1): # The last line is 'Estimated using ... for rate rule (originalTemplate)' #if from training reaction is in the first line append it to the end of the second line and skip the first line if not 'Average of' in kinetics.comment: - if 'from training reaction' in lines[0]: + if 'From training reaction' in lines[0]: comment = lines[1] else: comment = lines[0] @@ -2227,6 +2232,8 @@ def assignWeightsToEntries(entryNestedList, weightedEntries, N = 1): training = {} for tokenTemplateLabel, weight in weightedEntries: + if 'From training reaction' in tokenTemplateLabel: + tokenTemplateLabel = tokenTemplateLabel.split()[-1] ruleEntry, trainingEntry = self.retrieveOriginalEntry(tokenTemplateLabel) if trainingEntry: if (ruleEntry, trainingEntry) in training: diff --git a/rmgpy/data/kinetics/kineticsTest.py b/rmgpy/data/kinetics/kineticsTest.py index 85275c110b..740821a543 100644 --- a/rmgpy/data/kinetics/kineticsTest.py +++ b/rmgpy/data/kinetics/kineticsTest.py @@ -26,18 +26,22 @@ ################################################################################ import os -import unittest -import itertools +import unittest +from external.wip import work_in_progress + import numpy from rmgpy import settings from rmgpy.chemkin import loadChemkinFile -from rmgpy.data.kinetics.database import KineticsDatabase from rmgpy.data.base import Entry, DatabaseError, ForbiddenStructures +from rmgpy.data.kinetics.common import saveEntry, filter_reactions, find_degenerate_reactions, ensure_independent_atom_ids +from rmgpy.data.kinetics.database import KineticsDatabase +from rmgpy.data.kinetics.family import TemplateReaction from rmgpy.data.rmg import RMGDatabase -from rmgpy.rmg.react import findDegeneracies, react, reactSpecies from rmgpy.molecule.molecule import Molecule from rmgpy.species import Species + + ################################################### def setUpModule(): @@ -104,110 +108,99 @@ def setUpClass(self): global database self.database = database - def testR_Addition_MultipleBondBenzene(self): - """Test that the proper degeneracy is calculated for H addition to benzene""" - family = 'R_Addition_MultipleBond' - reactants = [ - Molecule().fromSMILES('c1ccccc1'), - Molecule().fromSMILES('[H]'), - ] - # assign atom IDs - for reactant in reactants: reactant.assignAtomIDs() + def assert_correct_reaction_degeneracy(self, reactants, expected_rxn_num, expected_degeneracy, + family_label=None, products=None, adjlists=False): + """ + Generates reactions for the provided species and checks the results + against the expected values. + + Args: + reactants: list of SMILES for the reacting species + family_label: label of the reaction family to react in + expected_rxn_num: number of independent reaction expected + expected_degeneracy: set of expected degeneracy values + products: list of SMILES for the desired products (optional) + adjlists: bool indicating if the input format is adjacency lists (optional) + assumes that the input is SMILES if False or unspecified + + Returns: + list of the generated reactions for further analysis if desired + """ + method = Molecule.fromAdjacencyList if adjlists else Molecule.fromSMILES - reactants = [mol.generateResonanceIsomers() for mol in reactants] + reactants = [method(Molecule(), identifier) for identifier in reactants] + if products is not None: + products = [method(Molecule(), identifier) for identifier in products] + else: + products = None - combinations = itertools.product(reactants[0], reactants[1]) + families = [family_label] if family_label is not None else None - reactionList = [] - for combi in combinations: - reactionList.extend(self.database.kinetics.families[family].generateReactions(combi)) + reaction_list = self.database.kinetics.generate_reactions_from_families(reactants, products, + only_families=families) - reactionList = findDegeneracies(reactionList) + self.assertEqual(len(reaction_list), expected_rxn_num, + 'Expected {0} reactions, not {1} for {2} in {3}.'.format(expected_rxn_num, + len(reaction_list), + reactants, + family_label)) - self.assertEqual(len(reactionList), 1) - for rxn in reactionList: - self.assertEqual(rxn.degeneracy, 6) + degeneracy = set([rxn.degeneracy for rxn in reaction_list]) - def testR_Addition_MultipleBondMethylNaphthalene(self): - """Test that the proper degeneracy is calculated for H addition to methylnaphthalene""" - family = 'R_Addition_MultipleBond' - reactants = [ - Molecule().fromSMILES('C1=CC=C2C=CC=CC2=C1C'), - Molecule().fromSMILES('[H]'), - ] - # assign atom IDs - for reactant in reactants: reactant.assignAtomIDs() - - reactants = [mol.generateResonanceIsomers() for mol in reactants] + self.assertEqual(degeneracy, expected_degeneracy, + 'Expected degeneracies of {0}, not {1} for {2} in {3}.'.format(expected_degeneracy, + degeneracy, + reactants, + family_label)) - combinations = itertools.product(reactants[0], reactants[1]) + return reaction_list - reactionList = [] - for combi in combinations: - reactionList.extend(self.database.kinetics.families[family].generateReactions(combi)) + def testR_Addition_MultipleBondBenzene(self): + """Test that the proper degeneracy is calculated for H addition to benzene""" + family_label = 'R_Addition_MultipleBond' + reactants = ['c1ccccc1', '[H]'] - product = Species().fromSMILES('C[C]1CC=CC2=CC=CC=C12') - product.generateResonanceIsomers() + correct_rxn_num = 1 + correct_degeneracy = {6} - targetReactions = [] - for rxn in reactionList: - for spc in rxn.products: - if product.isIsomorphic(spc): - targetReactions.append(rxn) + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label) - targetReactions = findDegeneracies(targetReactions) + def testR_Addition_MultipleBondMethylNaphthalene(self): + """Test that the proper degeneracy is calculated for H addition to methylnaphthalene""" + family_label = 'R_Addition_MultipleBond' + reactants = ['C1=CC=C2C=CC=CC2=C1C', '[H]'] + products = ['C[C]1CC=CC2=CC=CC=C12'] - self.assertEqual(len(targetReactions), 1) - for rxn in targetReactions: - self.assertEqual(rxn.degeneracy, 1) + correct_rxn_num = 1 + correct_degeneracy = {1} + + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label, products) def testR_RecombinationPhenyl(self): """Test that the proper degeneracy is calculated for phenyl + H recombination""" - family = 'R_Recombination' - reactants = [ - Molecule().fromSMILES('[c]1ccccc1'), - Molecule().fromSMILES('[H]'), - ] + family_label = 'R_Recombination' + reactants = ['[c]1ccccc1', '[H]'] - # assign atom IDs - for reactant in reactants: reactant.assignAtomIDs() + correct_rxn_num = 1 + correct_degeneracy = {1} - reactants = [mol.generateResonanceIsomers() for mol in reactants] - - combinations = itertools.product(reactants[0], reactants[1]) - - reactionList = [] - for combi in combinations: - reactionList.extend(self.database.kinetics.families[family].generateReactions(combi)) - - reactionList = findDegeneracies(reactionList) - - self.assertEqual(len(reactionList), 1) - for rxn in reactionList: - self.assertEqual(rxn.degeneracy, 1) + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label) def testR_RecombinationH(self): """Test that the proper degeneracy is calculated for H + H recombination""" - family = 'R_Recombination' - reactants = [ - Molecule().fromSMILES('[H]'), - Molecule().fromSMILES('[H]'), - ] - for reactant in reactants: reactant.assignAtomIDs() + family_label = 'R_Recombination' + reactants = ['[H]', '[H]'] - reactionList = self.database.kinetics.families[family].generateReactions(reactants) + correct_rxn_num = 1 + correct_degeneracy = {0.5} - reactionList = findDegeneracies(reactionList) - - self.assertEqual(len(reactionList), 1) - self.assertEqual(reactionList[0].degeneracy, 0.5) + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label) def test_degeneracy_for_methyl_methyl_recombination(self): """Test that the proper degeneracy is calculated for methyl + methyl recombination""" - correct_degeneracy = 0.5 - rxn_family_str = 'R_Recombination' - adj_lists = [ + family_label = 'R_Recombination' + reactants = [ """ multiplicity 2 1 C u1 p0 c0 {2,S} {3,S} {4,S} @@ -224,14 +217,16 @@ def test_degeneracy_for_methyl_methyl_recombination(self): """ ] - self.compare_degeneracy_of_reaction(adj_lists,rxn_family_str,correct_degeneracy) + correct_rxn_num = 1 + correct_degeneracy = {0.5} + + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label, adjlists=True) def test_degeneracy_for_methyl_labeled_methyl_recombination(self): """Test that the proper degeneracy is calculated for methyl + labeled methyl recombination""" - correct_degeneracy = 1 - rxn_family_str = 'R_Recombination' - adj_lists = [ + family_label = 'R_Recombination' + reactants = [ """ multiplicity 2 1 C u1 p0 c0 {2,S} {3,S} {4,S} @@ -248,14 +243,16 @@ def test_degeneracy_for_methyl_labeled_methyl_recombination(self): """ ] - self.compare_degeneracy_of_reaction(adj_lists,rxn_family_str,correct_degeneracy) + correct_rxn_num = 1 + correct_degeneracy = {1} + + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label, adjlists=True) def test_degeneracy_for_ethyl_ethyl_disproportionation(self): """Test that the proper degeneracy is calculated for ethyl + ethyl disproportionation""" - correct_degeneracy = 3 - rxn_family_str = 'Disproportionation' - adj_lists = [ + family_label = 'Disproportionation' + reactants = [ """ multiplicity 2 1 C u0 p0 c0 {2,S} {5,S} {6,S} {7,S} @@ -278,14 +275,16 @@ def test_degeneracy_for_ethyl_ethyl_disproportionation(self): """ ] - self.compare_degeneracy_of_reaction(adj_lists,rxn_family_str,correct_degeneracy) + correct_rxn_num = 1 + correct_degeneracy = {3} + + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label, adjlists=True) def test_degeneracy_for_ethyl_labeled_ethyl_disproportionation(self): """Test that the proper degeneracy is calculated for ethyl + labeled ethyl disproportionation""" - correct_degeneracy = 3 - rxn_family_str = 'Disproportionation' - adj_lists = [ + family_label = 'Disproportionation' + reactants = [ """ multiplicity 2 1 C u0 p0 c0 i13 {2,S} {5,S} {6,S} {7,S} @@ -307,109 +306,50 @@ def test_degeneracy_for_ethyl_labeled_ethyl_disproportionation(self): 7 H u0 p0 c0 {1,S} """ ] - expected_products = 2 - self.compare_degeneracy_of_reaction(adj_lists,rxn_family_str,correct_degeneracy * expected_products,expected_products) - - def compare_degeneracy_of_reaction(self, reactants_adj_list, - rxn_family_str, - num_expected_degenerate_products, - num_independent_reactions = 1): - """ - given: - `reactants_adj_list`: a list of adjacency lists (of reactants) - `reaction_family_str`: the string representation of the reaction family - `num_expected_degenerate_products`: the total number of degenerate reactions - which should be found by generateReactions. - `num_independent_rxns`: the number of reaction objects expected from generateReactions + correct_rxn_num = 2 + correct_degeneracy = {3} - performs: + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label, adjlists=True) - a check to ensure that the number of degenerate reactions is what is - expected. + @work_in_progress + def test_degeneracy_does_not_include_identical_atom_labels(self): """ + Test that rxns with identical atom ids are not counted twice for degeneracy - found_degeneracy, reaction = self.find_reaction_degeneracy(reactants_adj_list,rxn_family_str, - num_independent_reactions) - self.assertEqual(found_degeneracy, num_expected_degenerate_products,'degeneracy returned ({0}) is not the correct value ({1}) for reaction {2}'.format(found_degeneracy, num_expected_degenerate_products,reaction)) + Uses [H] + CC=C[CH]C -> H2 + [CH2]C=C[CH]C as an example. Since the reactant + is symmetric, there should be a single reaction with a degeneracy of 6. - def find_reaction_degeneracy(self, reactants_adj_list,rxn_family_str, - num_independent_reactions = 1): + Marked work_in_progress because the current multiple TS algorithm will + differentiate the reactions based on template, resulting in 2 reactions + each with a degeneracy of 6. """ - given: - - reactants_adj_list: a list of adjacency lists of the reactants - `reaction_family_str`: the string representation of the reaction family - `num_independent_rxns`: the number of reaction objects expected from generateReactions - returns: + family_label = 'H_Abstraction' + reactants = ['[H]', 'CC=C[CH]C'] + products = ['[H][H]', '[CH2]C=C[CH]C'] - a tuple with the total degeneracy and a list of reaction objects - """ - family = self.database.kinetics.families[rxn_family_str] - reactants = [Molecule().fromAdjacencyList(reactants_adj_list[0]), - Molecule().fromAdjacencyList(reactants_adj_list[1])] + correct_rxn_num = 1 + correct_degeneracy = {6} - for reactant in reactants: reactant.assignAtomIDs() - reactions = family.generateReactions(reactants) - reactions = findDegeneracies(reactions) - self.assertEqual(len(reactions), num_independent_reactions,'only {1} reaction(s) should be produced. Produced reactions {0}'.format(reactions,num_independent_reactions)) + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label, products=products) - return sum([reaction.degeneracy for reaction in reactions]), reactions - - def test_degeneracy_does_not_include_identical_atom_labels(self): - """ - ensure rxns with identical atom_ids are not counted twice for degeneracy - - this test uses [H] + CC=C[CH]C -> H2 + [CH2]C=C[CH]C as an example. Since - the reactant is symmetric with the middle carbon, the degeneracy should be - 6. - """ - spcA = Species().fromSMILES('[H]') - spcB = Species().fromSMILES('CC=C[CH]C') - spcB.generateResonanceIsomers(keepIsomorphic=True) - spcTuples = [(spcA,spcB)] - - reactionList = list(react(*spcTuples)) - - # find reaction with a specific product - specific_product = Species().fromSMILES('[CH2]C=C[CH]C') - - specific_product.generateResonanceIsomers() - - specific_reaction = None - for rxn in reactionList: - if any([specific_product.isIsomorphic(product) for product in rxn.products]): - specific_reaction = rxn - break - self.assertIsNotNone(specific_reaction,'no reaction found with the specified product') - - self.assertEqual(specific_reaction.degeneracy, 6,'The reaction output the wrong degeneracy of {}.'.format(specific_reaction.degeneracy)) def test_degeneracy_keeps_separate_transition_states_separated(self): """ - ensure rxns with multiple transition states are kept as separate reactions + Test that rxns with multiple transition states are kept as separate reactions - this test uses C[C]=C + C=C[CH2] -> C=C=C + C=CC as an example. - This reaction should have two transition states, which should occur regardless - of the order . + Uses C[C]=C + C=C[CH2] -> C=C=C + C=CC as an example. This reaction should have + two transition states, which should occur regardless of reactant order. """ - spcA = Species().fromSMILES('C[C]=C') - spcB = Species().fromSMILES('C=C[CH2]') - spcTuples = [(spcA,spcB)] - reactionList = list(react(*spcTuples)) - # find reaction with a specific product - specific_products = [Species().fromSMILES('C=C=C'), - Species().fromSMILES('CC=C'),] - - # eliminate rxns that do not match products - isomorphic_rxns = 0 - for rxn in reactionList: - # rxn contains all products - if all([any([specific_product.isIsomorphic(product) for product in rxn.products]) for specific_product in specific_products]): - isomorphic_rxns += 1 + family_label = 'Disproportionation' + reactants = ['C[C]=C', 'C=C[CH2]'] + products = ['C=C=C', 'CC=C'] + + correct_rxn_num = 2 + correct_degeneracy = {1, 6} + + self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label, products=products) - self.assertEqual(isomorphic_rxns, 2,'The reaction output did not output all the transition states in either order of reactants') - def test_separate_transition_states_generated_regardless_of_reactant_order(self): """ ensure rxns with multiple transition states are kept as separate reactions @@ -443,35 +383,6 @@ def test_separate_transition_states_generated_regardless_of_reactant_order(self) self.assertEqual(reverseTemplates, templates,'The reaction output did not output all the transition states in either order of reactants') - def test_degeneracy_keeps_track_of_both_rate_rules_from_resonance_isomers(self): - """ - rxns that have multiple resonance structures hitting different rate rules should - be kept separate when findDegeneracy is used. - - this test uses [H] + CC=C[CH]C -> H2 + [CH2]C=C[CH]C as an example. - This reaction should have two transition states. - """ - spcA = Species().fromSMILES('[H]') - spcB = Species().fromSMILES('CC=C[CH]C') - spcB.generateResonanceIsomers(keepIsomorphic=True) - spcTuples = [(spcA,spcB)] - - reactionList = list(react(*spcTuples)) - - # find reaction with a specific product - specific_product = Species().fromSMILES('CC=C[CH][CH2]') - specific_product.generateResonanceIsomers() - - specific_reactions_found = 0 - templates_found = [] - for rxn in reactionList: - if any([specific_product.isIsomorphic(product) for product in rxn.products]): - specific_reactions_found += 1 - templates_found.append(rxn.template) - - self.assertEqual(specific_reactions_found, 2,'The reaction output did not contain 2 transition states.') - self.assertNotEqual(templates_found[0],templates_found[1],'The reactions should have different templates') - def test_propyl_propyl_reaction_is_the_half_propyl_butyl(self): """ test that propyl propyl r-recombination is the same rate as propyl butyl @@ -480,63 +391,37 @@ def test_propyl_propyl_reaction_is_the_half_propyl_butyl(self): with identical reactants have half the reaction rate since there is a symmetrical transition state. """ - rxn_family_str = 'R_Recombination' - propyl_adj_list = """ - multiplicity 2 - 1 C u0 p0 c0 {2,S} {6,S} {7,S} {8,S} - 2 C u0 p0 c0 {1,S} {3,S} {9,S} {10,S} - 3 C u1 p0 c0 {2,S} {4,S} {5,S} - 4 H u0 p0 c0 {3,S} - 5 H u0 p0 c0 {3,S} - 6 H u0 p0 c0 {1,S} - 7 H u0 p0 c0 {1,S} - 8 H u0 p0 c0 {1,S} - 9 H u0 p0 c0 {2,S} - 10 H u0 p0 c0 {2,S} - - """ - butyl_adj_list = """ - multiplicity 2 - 1 C u0 p0 c0 {2,S} {7,S} {8,S} {9,S} - 2 C u0 p0 c0 {1,S} {3,S} {10,S} {11,S} - 3 C u0 p0 c0 {2,S} {4,S} {12,S} {13,S} - 4 C u1 p0 c0 {3,S} {5,S} {6,S} - 5 H u0 p0 c0 {4,S} - 6 H u0 p0 c0 {4,S} - 7 H u0 p0 c0 {1,S} - 8 H u0 p0 c0 {1,S} - 9 H u0 p0 c0 {1,S} - 10 H u0 p0 c0 {2,S} - 11 H u0 p0 c0 {2,S} - 12 H u0 p0 c0 {3,S} - 13 H u0 p0 c0 {3,S} - """ + family_label = 'R_Recombination' + propyl = 'CC[CH2]' + butyl = 'CCC[CH2]' - family = self.database.kinetics.families[rxn_family_str] + rxn_list_pp = self.assert_correct_reaction_degeneracy([propyl, propyl], 1, {0.5}, family_label) + rxn_list_pb = self.assert_correct_reaction_degeneracy([propyl, butyl], 1, {1}, family_label) - # get reaction objects and their degeneracy - pp_degeneracy, pp_reactions = self.find_reaction_degeneracy([propyl_adj_list,propyl_adj_list],rxn_family_str) - pb_degeneracy, pb_reactions = self.find_reaction_degeneracy([propyl_adj_list,butyl_adj_list],rxn_family_str) + family = self.database.kinetics.families[family_label] - # since output is a list of 1 - pp_reaction = pp_reactions[0] - pb_reaction = pb_reactions[0] + pp_reaction = rxn_list_pp[0] + pb_reaction = rxn_list_pb[0] # get kinetics for each reaction pp_kinetics_list = family.getKinetics(pp_reaction, pp_reaction.template, degeneracy=pp_reaction.degeneracy, - estimator = 'rate rules') - self.assertEqual(len(pp_kinetics_list), 1, 'The propyl and propyl recombination should only return one reaction. It returned {0}. Here is the full kinetics: {1}'.format(len(pp_kinetics_list),pp_kinetics_list)) + estimator='rate rules') + self.assertEqual(len(pp_kinetics_list), 1, + 'The propyl and propyl recombination should only return one reaction. \ + It returned {0}. Here is the full kinetics: {1}'.format(len(pp_kinetics_list), pp_kinetics_list)) pb_kinetics_list = family.getKinetics(pb_reaction, pb_reaction.template, degeneracy=pb_reaction.degeneracy, - estimator = 'rate rules') - self.assertEqual(len(pb_kinetics_list), 1, 'The propyl and butyl recombination should only return one reaction. It returned {0}. Here is the full kinetics: {1}'.format(len(pb_kinetics_list),pb_kinetics_list)) + estimator='rate rules') + self.assertEqual(len(pb_kinetics_list), 1, + 'The propyl and butyl recombination should only return one reaction. \ + It returned {0}. Here is the full kinetics: {1}'.format(len(pb_kinetics_list), pb_kinetics_list)) # the same reaction group must be found or this test will not work - self.assertIn(pb_kinetics_list[0][0].comment,pp_kinetics_list[0][0].comment, - 'this test found different kinetics for the two groups, so it will not function as expected\n' + - str(pp_kinetics_list)+str(pb_kinetics_list)) + self.assertIn(pb_kinetics_list[0][0].comment, pp_kinetics_list[0][0].comment, + 'This test found different kinetics for the two groups, so it will not function as expected\n' + + str(pp_kinetics_list)+str(pb_kinetics_list)) # test that the kinetics are correct self.assertAlmostEqual(pp_kinetics_list[0][0].getRateCoefficient(300) * 2, pb_kinetics_list[0][0].getRateCoefficient(300)) @@ -552,120 +437,41 @@ def test_identical_reactants_have_similar_kinetics(self): This method should be more robust than just checking the degeneracy of reactions. """ - rxn_family_str = 'R_Addition_MultipleBond' - butenyl_adj_list = """ - multiplicity 2 - 1 C u0 p0 c0 {2,S} {3,S} {5,S} {6,S} - 2 C u0 p0 c0 {1,S} {4,D} {7,S} - 3 C u1 p0 c0 {1,S} {8,S} {9,S} - 4 C u0 p0 c0 {2,D} {10,S} {11,S} - 5 H u0 p0 c0 {1,S} - 6 H u0 p0 c0 {1,S} - 7 H u0 p0 c0 {2,S} - 8 H u0 p0 c0 {3,S} - 9 H u0 p0 c0 {3,S} - 10 H u0 p0 c0 {4,S} - 11 H u0 p0 c0 {4,S} - """ - pentenyl_adj_list = """ - multiplicity 2 - 1 C u0 p0 c0 {2,S} {3,S} {8,S} {9,S} - 2 C u0 p0 c0 {1,S} {4,S} {6,S} {7,S} - 3 C u0 p0 c0 {1,S} {5,D} {10,S} - 4 C u1 p0 c0 {2,S} {11,S} {12,S} - 5 C u0 p0 c0 {3,D} {13,S} {14,S} - 6 H u0 p0 c0 {2,S} - 7 H u0 p0 c0 {2,S} - 8 H u0 p0 c0 {1,S} - 9 H u0 p0 c0 {1,S} - 10 H u0 p0 c0 {3,S} - 11 H u0 p0 c0 {4,S} - 12 H u0 p0 c0 {4,S} - 13 H u0 p0 c0 {5,S} - 14 H u0 p0 c0 {5,S} - """ - - family = self.database.kinetics.families[rxn_family_str] - - # get reaction objects and their degeneracy - pp_degeneracy, pp_reactions = self.find_reaction_degeneracy([butenyl_adj_list,butenyl_adj_list],rxn_family_str, num_independent_reactions=2) - pb_degeneracy, pb_reactions = self.find_reaction_degeneracy([butenyl_adj_list,pentenyl_adj_list],rxn_family_str, num_independent_reactions=4) - - # find the correct reaction from the list - symmetric_product=Molecule().fromAdjacencyList(''' - multiplicity 3 - 1 C u0 p0 c0 {2,S} {3,S} {6,S} {9,S} - 2 C u0 p0 c0 {1,S} {4,S} {10,S} {11,S} - 3 C u0 p0 c0 {1,S} {7,S} {12,S} {13,S} - 4 C u0 p0 c0 {2,S} {5,S} {14,S} {15,S} - 5 C u0 p0 c0 {4,S} {8,D} {16,S} - 6 C u1 p0 c0 {1,S} {19,S} {20,S} - 7 C u1 p0 c0 {3,S} {17,S} {18,S} - 8 C u0 p0 c0 {5,D} {21,S} {22,S} - 9 H u0 p0 c0 {1,S} - 10 H u0 p0 c0 {2,S} - 11 H u0 p0 c0 {2,S} - 12 H u0 p0 c0 {3,S} - 13 H u0 p0 c0 {3,S} - 14 H u0 p0 c0 {4,S} - 15 H u0 p0 c0 {4,S} - 16 H u0 p0 c0 {5,S} - 17 H u0 p0 c0 {7,S} - 18 H u0 p0 c0 {7,S} - 19 H u0 p0 c0 {6,S} - 20 H u0 p0 c0 {6,S} - 21 H u0 p0 c0 {8,S} - 22 H u0 p0 c0 {8,S} - ''') - asymmetric_product = Molecule().fromAdjacencyList(''' - multiplicity 3 - 1 C u0 p0 c0 {2,S} {3,S} {7,S} {10,S} - 2 C u0 p0 c0 {1,S} {5,S} {11,S} {12,S} - 3 C u0 p0 c0 {1,S} {4,S} {13,S} {14,S} - 4 C u0 p0 c0 {3,S} {6,S} {17,S} {18,S} - 5 C u0 p0 c0 {2,S} {8,S} {15,S} {16,S} - 6 C u0 p0 c0 {4,S} {9,D} {19,S} - 7 C u1 p0 c0 {1,S} {22,S} {23,S} - 8 C u1 p0 c0 {5,S} {20,S} {21,S} - 9 C u0 p0 c0 {6,D} {24,S} {25,S} - 10 H u0 p0 c0 {1,S} - 11 H u0 p0 c0 {2,S} - 12 H u0 p0 c0 {2,S} - 13 H u0 p0 c0 {3,S} - 14 H u0 p0 c0 {3,S} - 15 H u0 p0 c0 {5,S} - 16 H u0 p0 c0 {5,S} - 17 H u0 p0 c0 {4,S} - 18 H u0 p0 c0 {4,S} - 19 H u0 p0 c0 {6,S} - 20 H u0 p0 c0 {8,S} - 21 H u0 p0 c0 {8,S} - 22 H u0 p0 c0 {7,S} - 23 H u0 p0 c0 {7,S} - 24 H u0 p0 c0 {9,S} - 25 H u0 p0 c0 {9,S} - ''') - - pp_reaction = next((reaction for reaction in pp_reactions if reaction.products[0].isIsomorphic(symmetric_product)),None) - pb_reaction = next((reaction for reaction in pb_reactions if reaction.products[0].isIsomorphic(asymmetric_product)),None) - - pp_kinetics_list = family.getKinetics(pp_reaction, pp_reaction.template, - degeneracy=pp_reaction.degeneracy, - estimator = 'rate rules') - self.assertEqual(len(pp_kinetics_list), 1, 'The propyl and propyl recombination should only return one reaction. It returned {0}. Here is the full kinetics: {1}'.format(len(pp_kinetics_list),pp_kinetics_list)) - - pb_kinetics_list = family.getKinetics(pb_reaction, pb_reaction.template, - degeneracy=pb_reaction.degeneracy, - estimator = 'rate rules') - self.assertEqual(len(pb_kinetics_list), 1, 'The propyl and butyl recombination should only return one reaction. It returned {0}. Here is the full kinetics: {1}'.format(len(pb_kinetics_list),pb_kinetics_list)) + family_label = 'R_Addition_MultipleBond' + butenyl = 'C=CC[CH2]' + pentenyl = 'C=CCC[CH2]' + symmetric_product = ['[CH2]CC([CH2])CCC=C'] + asymmetric_product = ['[CH2]CCC([CH2])CCC=C'] + + rxn_list_bb = self.assert_correct_reaction_degeneracy([butenyl, butenyl], 1, {1}, family_label, products=symmetric_product) + rxn_list_bp = self.assert_correct_reaction_degeneracy([butenyl, pentenyl], 1, {1}, family_label, products=asymmetric_product) + + family = self.database.kinetics.families[family_label] + + bb_reaction = rxn_list_bb[0] + bp_reaction = rxn_list_bp[0] + + bb_kinetics_list = family.getKinetics(bb_reaction, bb_reaction.template, + degeneracy=bb_reaction.degeneracy, + estimator='rate rules') + self.assertEqual(len(bb_kinetics_list), 1, + 'The butenyl and butenyl addition should only return one reaction. \ + It returned {0}. Here is the full kinetics: {1}'.format(len(bb_kinetics_list), bb_kinetics_list)) + + bp_kinetics_list = family.getKinetics(bp_reaction, bp_reaction.template, + degeneracy=bp_reaction.degeneracy, + estimator='rate rules') + self.assertEqual(len(bp_kinetics_list), 1, + 'The butenyl and pentenyl addition should only return one reaction. \ + It returned {0}. Here is the full kinetics: {1}'.format(len(bp_kinetics_list), bp_kinetics_list)) # the same reaction group must be found or this test will not work - self.assertIn(pb_kinetics_list[0][0].comment,pp_kinetics_list[0][0].comment, - 'this test found different kinetics for the two groups, so it will not function as expected\n' + - str(pp_kinetics_list)+str(pb_kinetics_list)) + self.assertIn(bp_kinetics_list[0][0].comment, bb_kinetics_list[0][0].comment, + 'This test found different kinetics for the two groups, so it will not function as expected\n' + + str(bb_kinetics_list)+str(bp_kinetics_list)) # test that the kinetics are correct - self.assertAlmostEqual(pp_kinetics_list[0][0].getRateCoefficient(300), pb_kinetics_list[0][0].getRateCoefficient(300)) + self.assertAlmostEqual(bb_kinetics_list[0][0].getRateCoefficient(300), bp_kinetics_list[0][0].getRateCoefficient(300)) def test_reaction_degeneracy_independent_of_generatereactions_direction(self): """ @@ -691,33 +497,24 @@ def test_reaction_degeneracy_independent_of_generatereactions_direction(self): forward_reactions = family._KineticsFamily__generateReactions([molA, molB], products=[molC, molD], forward=True) reverse_reactions = family._KineticsFamily__generateReactions([molC, molD], products=[molA, molB], forward=False) - forward_reactions = findDegeneracies(forward_reactions) - reverse_reactions = findDegeneracies(reverse_reactions) + forward_reactions = find_degenerate_reactions(forward_reactions) + reverse_reactions = find_degenerate_reactions(reverse_reactions) self.assertEqual(forward_reactions[0].degeneracy, reverse_reactions[0].degeneracy, 'the kinetics from forward and reverse directions had different degeneracies, {} and {} respectively'.format(forward_reactions[0].degeneracy, reverse_reactions[0].degeneracy)) def test_degeneracy_same_reactant_different_resonance_structure(self): """Test if degeneracy is correct when reacting different resonance structures.""" - from rmgpy.reaction import _isomorphicSpeciesList - - spc = Species().fromSMILES('CC=C[CH2]') - # reactSpecies will label reactants and generate resonance structures - reactions = reactSpecies((spc, spc)) - # these products are only possible if the reacting structures are CC=C[CH2] and C[CH2]C=C - products = [Species().fromSMILES('CC=CC'), Species().fromSMILES('C=CC=C')] - - # search for the desired products - desired_rxn = None - for rxn in reactions: - if rxn.family == 'Disproportionation' and _isomorphicSpeciesList(rxn.products, products): - if desired_rxn is None: - desired_rxn = rxn - else: - self.fail('Found two reactions which should be isomorphic.') - - self.assertEqual(desired_rxn.degeneracy, 3) - self.assertEqual(set(desired_rxn.template), {'C_rad/H2/Cd', 'Cmethyl_Csrad/H/Cd'}) + family_label = 'Disproportionation' + reactants = ['CC=C[CH2]', 'CC=C[CH2]'] + products = ['CC=CC', 'C=CC=C'] + + correct_rxn_num = 1 + correct_degeneracy = {3} + + reaction_list = self.assert_correct_reaction_degeneracy(reactants, correct_rxn_num, correct_degeneracy, family_label, products) + + self.assertEqual(set(reaction_list[0].template), {'C_rad/H2/Cd', 'Cmethyl_Csrad/H/Cd'}) class TestKineticsCommentsParsing(unittest.TestCase): @@ -818,22 +615,17 @@ def setUpClass(self): global database self.database = database - for family in self.database.kinetics.families.values(): - family.addKineticsRulesFromTrainingSet(thermoDatabase=self.database.thermo) - family.fillKineticsRulesByAveragingUp(verbose=True) - - self.species, self.reactions = loadChemkinFile(os.path.join(settings['test_data.directory'], 'parsing_data','chem_annotated.inp'), - os.path.join(settings['test_data.directory'], 'parsing_data','species_dictionary.txt') - ) + self.species, self.reactions = loadChemkinFile( + os.path.join(settings['test_data.directory'], 'parsing_data', 'chem_annotated.inp'), + os.path.join(settings['test_data.directory'], 'parsing_data', 'species_dictionary.txt') + ) - def testFilterReactions(self): + def test_filter_reactions(self): """ tests that filter reactions removes reactions that are missing any reactants or products """ - from rmgpy.data.kinetics.common import filterReactions - reactions=self.reactions reactants = [] @@ -853,7 +645,7 @@ def testFilterReactions(self): newmreactants = list(mreactants-mlrset) newmproducts = list(mproducts-mlrset) - out = filterReactions(newmreactants,newmproducts,reactions) + out = filter_reactions(newmreactants, newmproducts, reactions) rset = list(set(reactions) - set(out)) @@ -866,14 +658,37 @@ def testFilterReactions(self): for i, iset in enumerate(outsets): #test that all the reactions left in aren't missing any reactants or products self.assertTrue(iset & lrset == set(),msg='reaction {0} left in improperly, should have removed in based on presence of {1}'.format(out[i],iset & lrset)) - - + + def test_react_molecules(self): + """ + Test that reaction generation for Molecule objects works. + """ + + moleculeTuple = (Molecule(SMILES='CC'), Molecule(SMILES='[CH3]')) + + reactionList = self.database.kinetics.react_molecules(moleculeTuple) + + self.assertIsNotNone(reactionList) + self.assertTrue(all([isinstance(rxn, TemplateReaction) for rxn in reactionList])) + + def test_ensure_independent_atom_ids(self): + """ + Ensure ensure_independent_atom_ids modifies atomlabels + """ + s1 = Species().fromSMILES('CCC') + s2 = Species().fromSMILES('C=C[CH]C') + self.assertEqual(s2.molecule[0].atoms[0].id, -1) + + ensure_independent_atom_ids([s1, s2]) + # checks atom id + self.assertNotEqual(s2.molecule[0].atoms[0].id, -1) + # checks second resonance structure id + self.assertNotEqual(s2.molecule[1].atoms[0].id, -1) + def testSaveEntry(self): """ tests that save entry can run """ - from rmgpy.data.kinetics.common import saveEntry - reactions=self.reactions fname = 'testfile.txt' @@ -957,8 +772,8 @@ def testaddReverseAttribute(self): r2 = Species(molecule=[Molecule().fromAdjacencyList(adjlist[1])]) p1 = Species(molecule=[Molecule().fromAdjacencyList(adjlist[2])]) p2 = Species(molecule=[Molecule().fromAdjacencyList(adjlist[3])]) - r1.generateResonanceIsomers(keepIsomorphic=True) - p1.generateResonanceIsomers(keepIsomorphic=True) + r1.generate_resonance_structures(keepIsomorphic=True) + p1.generate_resonance_structures(keepIsomorphic=True) rxn = TemplateReaction(reactants = [r1, r2], @@ -970,4 +785,76 @@ def testaddReverseAttribute(self): family.addReverseAttribute(rxn) - self.assertEqual(rxn.reverse.degeneracy, 6) \ No newline at end of file + self.assertEqual(rxn.reverse.degeneracy, 6) + + def test_generate_reactions_from_families_with_resonance(self): + """Test that we can generate reactions from families with resonance structures""" + reactants = [ + Molecule().fromSMILES('CC=C[CH2]'), + Molecule().fromSMILES('[OH]'), + ] + expected_product_1 = Molecule().fromSMILES('CC=CCO') + expected_product_2 = Molecule().fromSMILES('CC(O)C=C') + + reaction_list = self.database.kinetics.generate_reactions_from_families(reactants, only_families=['R_Recombination'], resonance=True) + + self.assertEqual(len(reaction_list), 2) + + case_1 = reaction_list[0].products[0].isIsomorphic(expected_product_1) and reaction_list[1].products[0].isIsomorphic(expected_product_2) + case_2 = reaction_list[0].products[0].isIsomorphic(expected_product_2) and reaction_list[1].products[0].isIsomorphic(expected_product_1) + + # Only one case should be true + self.assertTrue(case_1 ^ case_2) + + def test_generate_reactions_from_families_no_resonance(self): + """Test that we can generate reactions from families without resonance structures""" + reactants = [ + Molecule().fromSMILES('CC=C[CH2]'), + Molecule().fromSMILES('[OH]'), + ] + expected_product = Molecule().fromSMILES('CC=CCO') + + reaction_list = self.database.kinetics.generate_reactions_from_families(reactants, only_families=['R_Recombination'], resonance=False) + + self.assertEqual(len(reaction_list), 1) + + self.assertTrue(reaction_list[0].products[0].isIsomorphic(expected_product)) + + def test_generate_reactions_from_families_product_resonance(self): + """Test that we can specify the product resonance structure when generating reactions""" + reactants = [ + Molecule().fromSMILES('CCC=C'), + Molecule().fromSMILES('[H]'), + ] + products = [ + Molecule().fromSMILES('CC=C[CH2]'), + Molecule().fromSMILES('[H][H]'), + ] + + reaction_list = self.database.kinetics.generate_reactions_from_families(reactants, products, only_families=['H_Abstraction'], resonance=True) + + self.assertEqual(len(reaction_list), 1) + self.assertEqual(reaction_list[0].degeneracy, 2) + + reaction_list = self.database.kinetics.generate_reactions_from_families(reactants, products, only_families=['H_Abstraction'], resonance=False) + + self.assertEqual(len(reaction_list), 0) + + def test_generate_reactions_from_libraries(self): + """Test that we can generate reactions from libraries""" + reactants = [ + Molecule().fromSMILES('CC=O'), + Molecule().fromSMILES('[H]'), + ] + products = [ + Molecule().fromSMILES('[CH2]C=O'), + Molecule().fromSMILES('[H][H]'), + ] + + reaction_list = self.database.kinetics.generate_reactions_from_libraries(reactants) + + self.assertEqual(len(reaction_list), 3) + + reaction_list_2 = self.database.kinetics.generate_reactions_from_libraries(reactants, products) + + self.assertEqual(len(reaction_list_2), 1) diff --git a/rmgpy/data/rmg.py b/rmgpy/data/rmg.py index 081659c309..187be2a399 100644 --- a/rmgpy/data/rmg.py +++ b/rmgpy/data/rmg.py @@ -42,7 +42,7 @@ from rmgpy.data.kinetics.database import KineticsDatabase from statmech import StatmechDatabase from solvation import SolvationDatabase - +from rmgpy.exceptions import DatabaseError from rmgpy.scoop_framework.util import get, broadcast # Module-level variable to store the (only) instance of RMGDatabase in use. @@ -265,9 +265,9 @@ def getDB(name): if db: return db else: - raise Exception - except Exception, e: + raise DatabaseError + except DatabaseError, e: logging.debug("Did not find a way to obtain the broadcasted database for {}.".format(name)) raise e - raise Exception('Could not get database with name: {}'.format(name)) + raise DatabaseError('Could not get database with name: {}'.format(name)) diff --git a/rmgpy/data/solvation.py b/rmgpy/data/solvation.py index 244dbdc264..5a21946f9b 100644 --- a/rmgpy/data/solvation.py +++ b/rmgpy/data/solvation.py @@ -289,7 +289,7 @@ def loadEntry(self, except: logging.error("Can't understand '{0}' in solute library '{1}'".format(molecule, self.name)) raise - spc.generateResonanceIsomers() + spc.generate_resonance_structures() self.entries[label] = Entry( index = index, diff --git a/rmgpy/data/solvationTest.py b/rmgpy/data/solvationTest.py index b573a3ffe5..56f147edc3 100644 --- a/rmgpy/data/solvationTest.py +++ b/rmgpy/data/solvationTest.py @@ -53,12 +53,6 @@ def tearDown(self): import rmgpy.data.rmg rmgpy.data.rmg.database = None - from rmgpy.rmg.model import Species as DifferentSpecies - DifferentSpecies.solventData = None - DifferentSpecies.solventName = None - DifferentSpecies.solventStructure = None - DifferentSpecies.solventViscosity = None - def runTest(self): pass diff --git a/rmgpy/data/thermoTest.py b/rmgpy/data/thermoTest.py index ed432f5627..33115fe90e 100644 --- a/rmgpy/data/thermoTest.py +++ b/rmgpy/data/thermoTest.py @@ -233,7 +233,7 @@ def testSpeciesThermoGenerationHBILibrary(self): Ensure that molecule list is only reordered, and not changed after matching library value""" spec = Species().fromSMILES('C[CH]c1ccccc1') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() initial = list(spec.molecule) # Make a copy of the list thermo = self.database.getThermoData(spec) @@ -246,7 +246,7 @@ def testSpeciesThermoGenerationHBIGAV(self): Ensure that molecule list is only reordered, and not changed after group additivity""" spec = Species().fromSMILES('C[CH]c1ccccc1') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() initial = list(spec.molecule) # Make a copy of the list thermo = self.databaseWithoutLibraries.getThermoData(spec) @@ -283,7 +283,7 @@ def testSpeciesThermoGenerationLibrary(self): 20 H u0 p0 c0 {11,S} 21 H u0 p0 c0 {12,S} """) - spec.generateResonanceIsomers() + spec.generate_resonance_structures() self.assertTrue(arom.isIsomorphic(spec.molecule[1])) # The aromatic structure should be the second one @@ -301,7 +301,7 @@ def testThermoEstimationNotAffectDatabase(self): previous_enthalpy = poly_root.data.getEnthalpy(298)/4184.0 smiles = 'C1C2CC1C=CC=C2' spec = Species().fromSMILES(smiles) - spec.generateResonanceIsomers() + spec.generate_resonance_structures() thermo_gav = self.database.getThermoDataFromGroups(spec) _, polycyclicGroups = self.database.getRingGroupsFromComments(thermo_gav) @@ -364,7 +364,7 @@ def testNewThermoGeneration(self): for smiles, symm, H298, S298, Cp300, Cp400, Cp500, Cp600, Cp800, Cp1000, Cp1500 in self.testCases: Cplist = [Cp300, Cp400, Cp500, Cp600, Cp800, Cp1000, Cp1500] species = Species().fromSMILES(smiles) - species.generateResonanceIsomers() + species.generate_resonance_structures() thermoData = self.database.getThermoDataFromGroups(species) molecule = species.molecule[0] for mol in species.molecule[1:]: @@ -393,7 +393,7 @@ def testSymmetryNumberGeneration(self): """ for smiles, symm, H298, S298, Cp300, Cp400, Cp500, Cp600, Cp800, Cp1000, Cp1500 in self.testCases: species = Species().fromSMILES(smiles) - species.generateResonanceIsomers() + species.generate_resonance_structures() thermoData = self.database.getThermoDataFromGroups(species) # pick the molecule with lowest H298 molecule = species.molecule[0] @@ -432,7 +432,7 @@ def testLongDistanceInteractionInAromaticMolecule(self): Test long distance interaction is properly caculated for aromatic molecule. """ spec = Species().fromSMILES('c(O)1c(O)c(C=O)c(C=O)c(O)c(C=O)1') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() thermo = self.database.getThermoDataFromGroups(spec) self.assertIn('o_OH_OH', thermo.comment) @@ -448,7 +448,7 @@ def testLongDistanceInteractionInAromaticRadical(self): Test long distance interaction is properly caculated for aromatic radical. """ spec = Species().fromSMILES('c([O])1c(C=O)c(C=O)c(OC)cc1') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() thermo = self.database.getThermoDataFromGroups(spec) self.assertNotIn('o_OH_CHO', thermo.comment) @@ -464,7 +464,7 @@ def testLongDistanceInteractionInAromaticBiradical(self): Test long distance interaction is properly caculated for aromatic biradical. """ spec = Species().fromSMILES('c([O])1c([C]=O)cc(C=O)cc1') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() thermo = self.database.getThermoDataFromGroups(spec) thermo = self.database.getThermoDataFromGroups(spec) @@ -491,7 +491,7 @@ def testComputeGroupAdditivityThermoForTwoRingMolecule(self): give two different corrections accordingly. """ spec = Species().fromSMILES('CCCCCCCCCCCC(CC=C1C=CC=CC1)c1ccccc1') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() thermo = self.database.getThermoDataFromGroups(spec) ringGroups, polycyclicGroups = self.database.getRingGroupsFromComments(thermo) @@ -508,7 +508,7 @@ def testThermoForMonocyclicAndPolycyclicSameMolecule(self): Test a molecule that has both a polycyclic and a monocyclic ring in the same molecule """ spec = Species().fromSMILES('C(CCC1C2CCC1CC2)CC1CCC1') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() thermo = self.database.getThermoDataFromGroups(spec) ringGroups, polycyclicGroups = self.database.getRingGroupsFromComments(thermo) self.assertEqual(len(ringGroups),1) @@ -625,7 +625,7 @@ def testAddPolyRingCorrectionThermoDataFromHeuristicUsingPyrene(self): # then saturate it. smiles = 'C1C=C2C=CC=C3C=CC4=CC=CC=1C4=C23' spe = Species().fromSMILES(smiles) - spe.generateResonanceIsomers() + spe.generate_resonance_structures() mols = [] for mol in spe.molecule: sssr0 = mol.getSmallestSetOfSmallestRings() @@ -668,11 +668,11 @@ def testAddPolyRingCorrectionThermoDataFromHeuristicUsingAromaticTricyclic(self) # # creating it seems not natural in RMG, that's because # RMG cannot parse the adjacencyList of that isomer correctly - # so here we start with kekulized version and generateResonanceIsomers + # so here we start with kekulized version and generate_resonance_structures # and pick the one with two aromatic rings smiles = 'C1=CC2C=CC=C3C=CC(=C1)C=23' spe = Species().fromSMILES(smiles) - spe.generateResonanceIsomers() + spe.generate_resonance_structures() for mol in spe.molecule: sssr0 = mol.getSmallestSetOfSmallestRings() aromaticRingNum = 0 @@ -1027,7 +1027,7 @@ def testFindAromaticBondsFromSubMolecule(self): smiles = "C1=CC=C2C=CC=CC2=C1" spe = Species().fromSMILES(smiles) - spe.generateResonanceIsomers() + spe.generate_resonance_structures() mol = spe.molecule[1] # get two SSSRs @@ -1054,7 +1054,7 @@ def testBicyclicDecompositionForPolyringUsingPyrene(self): # then saturate it. smiles = 'C1C=C2C=CC=C3C=CC4=CC=CC=1C4=C23' spe = Species().fromSMILES(smiles) - spe.generateResonanceIsomers() + spe.generate_resonance_structures() for mol in spe.molecule: sssr0 = mol.getSmallestSetOfSmallestRings() aromaticRingNum = 0 @@ -1098,11 +1098,11 @@ def testBicyclicDecompositionForPolyringUsingAromaticTricyclic(self): # # creating it seems not natural in RMG, that's because # RMG cannot parse the adjacencyList of that isomer correctly - # so here we start with kekulized version and generateResonanceIsomers + # so here we start with kekulized version and generate_resonance_structures # and pick the one with two aromatic rings smiles = 'C1=CC2C=CC=C3C=CC(=C1)C=23' spe = Species().fromSMILES(smiles) - spe.generateResonanceIsomers() + spe.generate_resonance_structures() for mol in spe.molecule: sssr0 = mol.getSmallestSetOfSmallestRings() aromaticRingNum = 0 @@ -1276,14 +1276,14 @@ def testSaturateRingBonds2(self): """ smiles = 'C1=CC=C2CCCCC2=C1' spe = Species().fromSMILES(smiles) - spe.generateResonanceIsomers() + spe.generate_resonance_structures() mol = spe.molecule[1] ring_submol = convertRingToSubMolecule(mol.getDisparateRings()[1][0])[0] saturated_ring_submol, alreadySaturated = saturateRingBonds(ring_submol) expected_spe = Species().fromSMILES('C1=CC=C2CCCCC2=C1') - expected_spe.generateResonanceIsomers() + expected_spe.generate_resonance_structures() expected_saturated_ring_submol = expected_spe.molecule[1] # remove hydrogen expected_saturated_ring_submol.deleteHydrogens() @@ -1301,14 +1301,14 @@ def testSaturateRingBonds3(self): """ smiles = 'C1=CC=C2CC=CCC2=C1' spe = Species().fromSMILES(smiles) - spe.generateResonanceIsomers() + spe.generate_resonance_structures() mol = spe.molecule[1] ring_submol = convertRingToSubMolecule(mol.getDisparateRings()[1][0])[0] saturated_ring_submol, alreadySaturated = saturateRingBonds(ring_submol) expected_spe = Species().fromSMILES('C1=CC=C2CCCCC2=C1') - expected_spe.generateResonanceIsomers() + expected_spe.generate_resonance_structures() expected_saturated_ring_submol = expected_spe.molecule[1] # remove hydrogen expected_saturated_ring_submol.deleteHydrogens() diff --git a/rmgpy/molecule/generator.py b/rmgpy/molecule/generator.py index 0bd22ff1cc..18b66982e1 100644 --- a/rmgpy/molecule/generator.py +++ b/rmgpy/molecule/generator.py @@ -492,7 +492,7 @@ def find_lowest_u_layer(mol, u_layer, equivalent_atoms): def generate_minimum_resonance_isomer(mol): """ Select the resonance isomer that is isomorphic to the parameter isomer, with the lowest unpaired - electrons descriptor. + electrons descriptor, unless this unnecessarily forms a charged molecule. First, we generate all isomorphic resonance isomers. Next, we return the candidate with the lowest unpaired electrons metric. @@ -501,24 +501,29 @@ def generate_minimum_resonance_isomer(mol): """ cython.declare( + atom=Atom, candidates=list, sel=Molecule, cand=Molecule, metric_sel=list, + charge_sel=int, + charge_cand=int, metric_cand=list, ) + candidates = resonance.generate_isomorphic_resonance_structures(mol) - candidates = resonance.generateIsomorphicResonanceStructures(mol) - - sel = candidates[0] + sel = mol metric_sel = get_unpaired_electrons(sel) - for cand in candidates[1:]: - metric_cand = get_unpaired_electrons(cand) - if metric_cand < metric_sel: - sel = cand - metric_sel = metric_cand - + charge_sel = sum([abs(atom.charge) for atom in sel.vertices]) + for cand in candidates: + metric_cand = get_unpaired_electrons(cand) + if metric_cand < metric_sel: + charge_cand = sum([abs(atom.charge) for atom in cand.vertices]) + if charge_cand <= charge_sel: + sel = cand + metric_sel = metric_cand + charge_sel = charge_cand return sel diff --git a/rmgpy/molecule/generatorTest.py b/rmgpy/molecule/generatorTest.py index 270e16fbc8..aee6450ca5 100644 --- a/rmgpy/molecule/generatorTest.py +++ b/rmgpy/molecule/generatorTest.py @@ -95,7 +95,7 @@ def testC4H6(self): class InChIGenerationTest(unittest.TestCase): def compare(self, adjlist, aug_inchi): spc = Species(molecule=[Molecule().fromAdjacencyList(adjlist)]) - spc.generateResonanceIsomers() + spc.generate_resonance_structures() ignore_prefix = r"(InChI=1+)(S*)/" @@ -352,9 +352,9 @@ def test_singlet_vs_closed_shell(self): """ singlet = Species(molecule=[Molecule().fromAdjacencyList(adjlist_singlet)]) - singlet.generateResonanceIsomers() + singlet.generate_resonance_structures() closed_shell = Species(molecule=[Molecule().fromAdjacencyList(adjlist_closed_shell)]) - closed_shell.generateResonanceIsomers() + closed_shell.generate_resonance_structures() singlet_aug_inchi = singlet.getAugmentedInChI() closed_shell_aug_inchi = closed_shell.getAugmentedInChI() diff --git a/rmgpy/molecule/groupTest.py b/rmgpy/molecule/groupTest.py index c82a4c538e..30e5879a0d 100644 --- a/rmgpy/molecule/groupTest.py +++ b/rmgpy/molecule/groupTest.py @@ -1221,7 +1221,7 @@ def performSampMoleComparison(adjlist, answer_smiles): group2 = Group().fromAdjacencyList(adjlist2) result2 = group2.makeSampleMolecule() naphthaleneMolecule = Molecule().fromSMILES('C1=CC=C2C=CC=CC2=C1') - resonanceList2=naphthaleneMolecule.generateResonanceIsomers() + resonanceList2=naphthaleneMolecule.generate_resonance_structures() self.assertTrue(any([result2.isIsomorphic(x) for x in resonanceList2])) #test the creation of a positively charged species diff --git a/rmgpy/molecule/inchiparsingTest.py b/rmgpy/molecule/inchiparsingTest.py index dff7accf02..d7b28dc527 100644 --- a/rmgpy/molecule/inchiparsingTest.py +++ b/rmgpy/molecule/inchiparsingTest.py @@ -52,7 +52,7 @@ def compare(self, inchi, u_indices=None, p_indices = None): ConsistencyChecker.check_partial_charge(at) spc = Species(molecule=[mol]) - spc.generateResonanceIsomers() + spc.generate_resonance_structures() ignore_prefix = r"(InChI=1+)(S*)/" aug_inchi_expected = re.split(ignore_prefix, aug_inchi)[-1] @@ -230,7 +230,7 @@ def test_CCCO_triplet(self): mol = Molecule().fromAdjacencyList(adjlist) spc = Species(molecule=[mol]) - spc.generateResonanceIsomers() + spc.generate_resonance_structures() aug_inchi = spc.getAugmentedInChI() self.assertEqual(Species(molecule=[Molecule().fromAugmentedInChI(aug_inchi)]).isIsomorphic(spc), True) diff --git a/rmgpy/molecule/molecule.pxd b/rmgpy/molecule/molecule.pxd index 0b852062f7..13e0c3febd 100644 --- a/rmgpy/molecule/molecule.pxd +++ b/rmgpy/molecule/molecule.pxd @@ -213,7 +213,7 @@ cdef class Molecule(Graph): cpdef float calculateSymmetryNumber(self) except -1 - cpdef list generateResonanceIsomers(self, bint keepIsomorphic=?) + cpdef list generate_resonance_structures(self, bint keepIsomorphic=?) cpdef tuple getAromaticRings(self, list rings=?) diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index b5f6429728..2ee29461bd 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -1603,8 +1603,9 @@ def isArylRadical(self, aromaticRings=None): return total == aryl - def generateResonanceIsomers(self, keepIsomorphic=False): - return resonance.generateResonanceStructures(self, keepIsomorphic=keepIsomorphic) + def generate_resonance_structures(self, keepIsomorphic=False): + """Returns a list of resonance structures of the molecule.""" + return resonance.generate_resonance_structures(self, keepIsomorphic=keepIsomorphic) def getURL(self): """ diff --git a/rmgpy/molecule/moleculeTest.py b/rmgpy/molecule/moleculeTest.py index fb77cf659a..c86dd0f743 100644 --- a/rmgpy/molecule/moleculeTest.py +++ b/rmgpy/molecule/moleculeTest.py @@ -279,7 +279,7 @@ def testGetBondOrdersForAtom(self): """ m = Molecule().fromSMILES('C12C(C=CC=C1)=CC=CC=2') - isomers = m.generateResonanceIsomers() + isomers = m.generate_resonance_structures() for isomer in isomers: for atom in isomer.atoms: if atom.symbol == 'C': @@ -1293,7 +1293,7 @@ def testAromaticBenzene(self): Test the Molecule.isAromatic() method for Benzene. """ m = Molecule().fromSMILES('C1=CC=CC=C1') - isomers = m.generateResonanceIsomers() + isomers = m.generate_resonance_structures() self.assertTrue(any(isomer.isAromatic() for isomer in isomers)) def testAromaticNaphthalene(self): @@ -1301,7 +1301,7 @@ def testAromaticNaphthalene(self): Test the Molecule.isAromatic() method for Naphthalene. """ m = Molecule().fromSMILES('C12C(C=CC=C1)=CC=CC=2') - isomers = m.generateResonanceIsomers() + isomers = m.generate_resonance_structures() self.assertTrue(any(isomer.isAromatic() for isomer in isomers)) def testAromaticCyclohexane(self): @@ -1309,7 +1309,7 @@ def testAromaticCyclohexane(self): Test the Molecule.isAromatic() method for Cyclohexane. """ m = Molecule().fromSMILES('C1CCCCC1') - isomers = m.generateResonanceIsomers() + isomers = m.generate_resonance_structures() self.assertFalse(any(isomer.isAromatic() for isomer in isomers)) def testCountInternalRotorsEthane(self): diff --git a/rmgpy/molecule/parser.py b/rmgpy/molecule/parser.py index 562c488e57..cf064ee0f3 100644 --- a/rmgpy/molecule/parser.py +++ b/rmgpy/molecule/parser.py @@ -221,7 +221,7 @@ def __lookup(mol, identifier, type_identifier): except KeyError: return None -def check(mol, aug_inchi) : +def check(mol, aug_inchi): """ Check if the molecular structure is correct. @@ -237,6 +237,8 @@ def check(mol, aug_inchi) : ) ConsistencyChecker.check_multiplicity(mol.getRadicalCount(), mol.multiplicity) + inchi, u_indices, p_indices = inchiutil.decompose(str(aug_inchi)) + assert(mol.getRadicalCount() == len(u_indices)) for at in mol.atoms: ConsistencyChecker.check_partial_charge(at) diff --git a/rmgpy/molecule/pathfinder.py b/rmgpy/molecule/pathfinder.py index 87002ccbcf..c48ddc9b04 100644 --- a/rmgpy/molecule/pathfinder.py +++ b/rmgpy/molecule/pathfinder.py @@ -263,28 +263,20 @@ def findAllDelocalizationPaths(atom1): def findAllDelocalizationPathsLonePairRadical(atom1): """ Find all the delocalization paths of lone electron pairs next to the radical center indicated - by `atom1`. Used to generate resonance isomers. + by `atom1`. Used to generate resonance isomers in adjacent N and O as in NO2. """ cython.declare(paths=list) cython.declare(atom2=Atom, bond12=Bond) - - # No paths if atom1 is not a radical - if atom1.radicalElectrons <= 0: - return [] - - # In a first step we only consider nitrogen and oxygen atoms as possible radical centers - if not ((atom1.lonePairs == 0 and atom1.isNitrogen()) or(atom1.lonePairs == 2 and atom1.isOxygen())): - return [] - - # Find all delocalization paths + paths = [] - for atom2, bond12 in atom1.edges.items(): - # Only single bonds are considered - if bond12.isSingle(): - # Neighboring atom must posses a lone electron pair to loose it - if ((atom2.lonePairs == 1 and atom2.isNitrogen()) or (atom2.lonePairs == 3 and atom2.isOxygen())) and (atom2.radicalElectrons == 0): + if atom1.isNitrogen() and atom1.radicalElectrons >= 1 and atom1.lonePairs == 0: + for atom2, bond12 in atom1.edges.items(): + if atom2.isOxygen() and atom2.radicalElectrons == 0 and atom2.lonePairs == 3 and bond12.isSingle(): + paths.append([atom1, atom2]) + elif atom1.isOxygen() and atom1.radicalElectrons >= 1 and atom1.lonePairs == 2: + for atom2, bond12 in atom1.edges.items(): + if atom2.isNitrogen() and atom2.radicalElectrons == 0 and atom2.lonePairs == 1 and bond12.isSingle(): paths.append([atom1, atom2]) - return paths def findAllDelocalizationPathsN5dd_N5ts(atom1): diff --git a/rmgpy/molecule/resonance.pxd b/rmgpy/molecule/resonance.pxd index 3bb51ba9a7..e6e8017c7b 100644 --- a/rmgpy/molecule/resonance.pxd +++ b/rmgpy/molecule/resonance.pxd @@ -1,30 +1,30 @@ from .graph cimport Vertex, Edge, Graph from .molecule cimport Atom, Bond, Molecule -cpdef list populateResonanceAlgorithms(dict features=?) +cpdef list populate_resonance_algorithms(dict features=?) -cpdef dict analyzeMolecule(Molecule mol) +cpdef dict analyze_molecule(Molecule mol) -cpdef list generateResonanceStructures(Molecule mol, bint clarStructures=?, bint keepIsomorphic=?) +cpdef list generate_resonance_structures(Molecule mol, bint clarStructures=?, bint keepIsomorphic=?) -cpdef list _generateResonanceStructures(list molList, list methodList, bint keepIsomorphic=?, bint copy=?) +cpdef list _generate_resonance_structures(list molList, list methodList, bint keepIsomorphic=?, bint copy=?) -cpdef list generateAdjacentResonanceStructures(Molecule mol) +cpdef list generate_adjacent_resonance_structures(Molecule mol) -cpdef list generateLonePairRadicalResonanceStructures(Molecule mol) +cpdef list generate_lone_pair_radical_resonance_structures(Molecule mol) -cpdef list generateN5dd_N5tsResonanceStructures(Molecule mol) +cpdef list generate_N5dd_N5ts_resonance_structures(Molecule mol) -cpdef list generateIsomorphicResonanceStructures(Molecule mol) +cpdef list generate_isomorphic_resonance_structures(Molecule mol) -cpdef list generateAromaticResonanceStructures(Molecule mol, dict features=?) +cpdef list generate_aromatic_resonance_structures(Molecule mol, dict features=?) -cpdef list generateKekuleStructure(Molecule mol) +cpdef list generate_kekule_structure(Molecule mol) -cpdef list generateOppositeKekuleStructure(Molecule mol) +cpdef list generate_opposite_kekule_structure(Molecule mol) -cpdef list generateClarStructures(Molecule mol) +cpdef list generate_clar_structures(Molecule mol) -cpdef list _clarOptimization(Molecule mol, list constraints=?, maxNum=?) +cpdef list _clar_optimization(Molecule mol, list constraints=?, maxNum=?) -cpdef list _clarTransformation(Molecule mol, list ring) +cpdef list _clar_transformation(Molecule mol, list ring) diff --git a/rmgpy/molecule/resonance.py b/rmgpy/molecule/resonance.py index a00cb75dfd..0d76e6e9cf 100644 --- a/rmgpy/molecule/resonance.py +++ b/rmgpy/molecule/resonance.py @@ -32,19 +32,19 @@ This module contains methods for generation of resonance structures of molecules. The main function to generate all relevant resonance structures for a given -Molecule object is ``generateResonanceStructures``. It calls the necessary +Molecule object is ``generate_resonance_structures``. It calls the necessary functions for generating each type of resonance structure. Currently supported resonance types: - All species: - - ``generateAdjacentResonanceStructures``: single radical shift with double or triple bond - - ``generateLonePairRadicalResonanceStructures``: single radical shift with lone pair - - ``generateN5dd_N5tsResonanceStructures``: shift between nitrogen with two double bonds and single + triple bond + - ``generate_adjacent_resonance_structures``: single radical shift with double or triple bond + - ``generate_lone_pair_radical_resonance_structures``: single radical shift with lone pair + - ``generate_N5dd_N5ts_resonance_structures``: shift between nitrogen with two double bonds and single + triple bond - Aromatic species only: - - ``generateAromaticResonanceStructures``: fully delocalized structure, where all aromatic rings have benzene bonds - - ``generateKekuleStructure``: generate a single Kekule structure for an aromatic compound (single/double bond form) - - ``generateOppositeKekuleStructure``: for monocyclic aromatic species, rotate the double bond assignment - - ``generateClarStructures``: generate all structures with the maximum number of pi-sextet assignments + - ``generate_aromatic_resonance_structures``: fully delocalized structure, where all aromatic rings have benzene bonds + - ``generate_kekule_structure``: generate a single Kekule structure for an aromatic compound (single/double bond form) + - ``generate_opposite_kekule_structure``: for monocyclic aromatic species, rotate the double bond assignment + - ``generate_clar_structures``: generate all structures with the maximum number of pi-sextet assignments """ import cython @@ -57,11 +57,11 @@ import rmgpy.molecule.pathfinder as pathfinder from rmgpy.exceptions import ILPSolutionError, KekulizationError, AtomTypeError -def populateResonanceAlgorithms(features=None): +def populate_resonance_algorithms(features=None): """ Generate list of resonance structure algorithms relevant to the current molecule. - Takes a dictionary of features generated by analyzeMolecule(). + Takes a dictionary of features generated by analyze_molecule(). Returns a list of resonance algorithms. """ cython.declare(methodList=list) @@ -69,28 +69,28 @@ def populateResonanceAlgorithms(features=None): if features is None: methodList = [ - generateAdjacentResonanceStructures, - generateLonePairRadicalResonanceStructures, - generateN5dd_N5tsResonanceStructures, - generateAromaticResonanceStructures, - generateKekuleStructure, - generateOppositeKekuleStructure, - generateClarStructures, + generate_adjacent_resonance_structures, + generate_lone_pair_radical_resonance_structures, + generate_N5dd_N5ts_resonance_structures, + generate_aromatic_resonance_structures, + generate_kekule_structure, + generate_opposite_kekule_structure, + generate_clar_structures, ] else: # If the molecule is aromatic, then radical resonance has already been considered # If the molecule was falsely identified as aromatic, then isArylRadical will still accurately capture # cases where the radical is in an orbital that is orthogonal to the pi orbitals. if features['isRadical'] and not features['isAromatic'] and not features['isArylRadical']: - methodList.append(generateAdjacentResonanceStructures) + methodList.append(generate_adjacent_resonance_structures) if features['hasNitrogen']: - methodList.append(generateN5dd_N5tsResonanceStructures) + methodList.append(generate_N5dd_N5ts_resonance_structures) if features['hasLonePairs']: - methodList.append(generateLonePairRadicalResonanceStructures) + methodList.append(generate_lone_pair_radical_resonance_structures) return methodList -def analyzeMolecule(mol): +def analyze_molecule(mol): """ Identify key features of molecule important for resonance structure generation. @@ -126,7 +126,7 @@ def analyzeMolecule(mol): return features -def generateResonanceStructures(mol, clarStructures=True, keepIsomorphic=False): +def generate_resonance_structures(mol, clarStructures=True, keepIsomorphic=False): """ Generate and return all of the resonance structures for the input molecule. @@ -154,15 +154,19 @@ def generateResonanceStructures(mol, clarStructures=True, keepIsomorphic=False): molList = [mol] # Analyze molecule - features = analyzeMolecule(mol) + features = analyze_molecule(mol) - # Use generateAromaticResonanceStructures to check for false positives and negatives + # Use generate_aromatic_resonance_structures to check for false positives and negatives if features['isAromatic'] or (features['isCyclic'] and features['isRadical'] and not features['isArylRadical']): - newMolList = generateAromaticResonanceStructures(mol, features) + newMolList = generate_aromatic_resonance_structures(mol, features) if len(newMolList) == 0: # Encountered false positive, ie. the molecule is not actually aromatic features['isAromatic'] = False features['isPolycyclicAromatic'] = False + else: + features['isAromatic'] = True + if len(newMolList[0].getAromaticRings()[0]) > 1: + features['isPolycyclicAromatic'] = True else: newMolList = [] @@ -171,27 +175,27 @@ def generateResonanceStructures(mol, clarStructures=True, keepIsomorphic=False): if features['isRadical'] and not features['isArylRadical']: if features['isPolycyclicAromatic']: if clarStructures: - _generateResonanceStructures(newMolList, [generateKekuleStructure], keepIsomorphic) - _generateResonanceStructures(newMolList, [generateAdjacentResonanceStructures], keepIsomorphic) - _generateResonanceStructures(newMolList, [generateClarStructures], keepIsomorphic) + _generate_resonance_structures(newMolList, [generate_kekule_structure], keepIsomorphic) + _generate_resonance_structures(newMolList, [generate_adjacent_resonance_structures], keepIsomorphic) + _generate_resonance_structures(newMolList, [generate_clar_structures], keepIsomorphic) # Remove non-aromatic structures under the assumption that they aren't important resonance contributors newMolList = [m for m in newMolList if m.isAromatic()] else: pass else: - _generateResonanceStructures(newMolList, [generateKekuleStructure, - generateOppositeKekuleStructure]), keepIsomorphic - _generateResonanceStructures(newMolList, [generateAdjacentResonanceStructures], keepIsomorphic) + _generate_resonance_structures(newMolList, [generate_kekule_structure, + generate_opposite_kekule_structure], keepIsomorphic) + _generate_resonance_structures(newMolList, [generate_adjacent_resonance_structures], keepIsomorphic) elif features['isPolycyclicAromatic']: if clarStructures: - _generateResonanceStructures(newMolList, [generateClarStructures], keepIsomorphic) + _generate_resonance_structures(newMolList, [generate_clar_structures], keepIsomorphic) else: pass else: # The molecule is an aryl radical or stable mono-ring aromatic # In this case, generate the kekulized form - _generateResonanceStructures(newMolList, [generateKekuleStructure, - generateOppositeKekuleStructure], keepIsomorphic) + _generate_resonance_structures(newMolList, [generate_kekule_structure, + generate_opposite_kekule_structure], keepIsomorphic) # Check for isomorphism against the original molecule for i, newMol in enumerate(newMolList): @@ -208,12 +212,12 @@ def generateResonanceStructures(mol, clarStructures=True, keepIsomorphic=False): molList.extend(newMolList) # Generate remaining resonance structures - methodList = populateResonanceAlgorithms(features) - _generateResonanceStructures(molList, methodList, keepIsomorphic) + methodList = populate_resonance_algorithms(features) + _generate_resonance_structures(molList, methodList, keepIsomorphic) return molList -def _generateResonanceStructures(molList, methodList, keepIsomorphic=False, copy=False): +def _generate_resonance_structures(molList, methodList, keepIsomorphic=False, copy=False): """ Iteratively generate all resonance structures for a list of starting molecules using the specified methods. @@ -255,7 +259,7 @@ def _generateResonanceStructures(molList, methodList, keepIsomorphic=False, copy return molList -def generateAdjacentResonanceStructures(mol): +def generate_adjacent_resonance_structures(mol): """ Generate all of the resonance structures formed by one allyl radical shift. @@ -300,7 +304,7 @@ def generateAdjacentResonanceStructures(mol): return isomers -def generateLonePairRadicalResonanceStructures(mol): +def generate_lone_pair_radical_resonance_structures(mol): """ Generate all of the resonance structures formed by lone electron pair - radical shifts. """ @@ -347,7 +351,7 @@ def generateLonePairRadicalResonanceStructures(mol): return isomers -def generateN5dd_N5tsResonanceStructures(mol): +def generate_N5dd_N5ts_resonance_structures(mol): """ Generate all of the resonance structures formed by shifts between N5dd and N5ts. """ @@ -430,7 +434,7 @@ def generateN5dd_N5tsResonanceStructures(mol): return isomers -def generateAromaticResonanceStructures(mol, features=None): +def generate_aromatic_resonance_structures(mol, features=None): """ Generate the aromatic form of the molecule. For radicals, generates the form with the most aromatic rings. @@ -444,7 +448,7 @@ def generateAromaticResonanceStructures(mol, features=None): i=cython.int, counter=cython.int) if features is None: - features = analyzeMolecule(mol) + features = analyze_molecule(mol) if not features['isCyclic']: return [] @@ -457,14 +461,14 @@ def generateAromaticResonanceStructures(mol, features=None): # Then determine which ones are aromatic aromaticBonds = molecule.getAromaticRings(rings)[1] - # If the species is a radical and the number of aromatic rings is less than the number of total rings, - # then there is a chance that the radical can be shifted to a location that increases the number of aromatic rings. - if (features['isRadical'] and not features['isArylRadical']) and (len(aromaticBonds) < len(rings)): + # If the species is a radical, then there is a chance that the radical can be shifted + # to a location that increases the number of perceived aromatic rings. + if features['isRadical'] and not features['isArylRadical']: if molecule.isAromatic(): - kekuleList = generateKekuleStructure(molecule) + kekuleList = generate_kekule_structure(molecule) else: kekuleList = [molecule] - _generateResonanceStructures(kekuleList, [generateAdjacentResonanceStructures]) + _generate_resonance_structures(kekuleList, [generate_adjacent_resonance_structures]) maxNum = 0 molList = [] @@ -544,7 +548,7 @@ def generateAromaticResonanceStructures(mol, features=None): return newMolList -def generateKekuleStructure(mol): +def generate_kekule_structure(mol): """ Generate a kekulized (single-double bond) form of the molecule. The specific arrangement of double bonds is non-deterministic, and depends on RDKit. @@ -569,7 +573,7 @@ def generateKekuleStructure(mol): return [molecule] -def generateOppositeKekuleStructure(mol): +def generate_opposite_kekule_structure(mol): """ Generate the Kekule structure with opposite single/double bond arrangement for single ring aromatics. @@ -612,7 +616,7 @@ def generateOppositeKekuleStructure(mol): else: return [molecule] -def generateIsomorphicResonanceStructures(mol): +def generate_isomorphic_resonance_structures(mol): """ Select the resonance isomer that is isomorphic to the parameter isomer, with the lowest unpaired electrons descriptor. @@ -644,7 +648,7 @@ def generateIsomorphicResonanceStructures(mol): isomer = isomers[index] newIsomers = [] - for algo in populateResonanceAlgorithms(): + for algo in populate_resonance_algorithms(): newIsomers.extend(algo(isomer)) for newIsomer in newIsomers: @@ -662,7 +666,7 @@ def generateIsomorphicResonanceStructures(mol): return isomorphic_isomers -def generateClarStructures(mol): +def generate_clar_structures(mol): """ Generate Clar structures for a given molecule. @@ -675,7 +679,7 @@ def generateClarStructures(mol): return [] try: - output = _clarOptimization(mol) + output = _clar_optimization(mol) except ILPSolutionError: # The optimization algorithm did not work on the first iteration return [] @@ -702,7 +706,7 @@ def generateClarStructures(mol): # Then apply locations of aromatic sextets by converting to benzene bonds for index, ring in enumerate(aromaticRings): if y[index] == 1: - _clarTransformation(newmol, ring) + _clar_transformation(newmol, ring) try: newmol.updateAtomTypes() @@ -714,7 +718,7 @@ def generateClarStructures(mol): return molList -def _clarOptimization(mol, constraints=None, maxNum=None): +def _clar_optimization(mol, constraints=None, maxNum=None): """ Implements linear programming algorithm for finding Clar structures. This algorithm maximizes the number of Clar sextets within the constraints of molecular geometry and atom valency. @@ -854,14 +858,14 @@ def _clarOptimization(mol, constraints=None, maxNum=None): # Run optimization with additional constraints try: - innerSolutions = _clarOptimization(mol, constraints=constraints, maxNum=maxNum) + innerSolutions = _clar_optimization(mol, constraints=constraints, maxNum=maxNum) except ILPSolutionError: innerSolutions = [] return innerSolutions + [(molecule, aromaticRings, bonds, solution)] -def _clarTransformation(mol, aromaticRing): +def _clar_transformation(mol, aromaticRing): """ Performs Clar transformation for given ring in a molecule, ie. conversion to aromatic sextet. diff --git a/rmgpy/molecule/resonanceTest.py b/rmgpy/molecule/resonanceTest.py index acd02cf78e..878960476a 100644 --- a/rmgpy/molecule/resonanceTest.py +++ b/rmgpy/molecule/resonanceTest.py @@ -31,20 +31,20 @@ from .molecule import Molecule from .resonance import * -from .resonance import _clarOptimization, _clarTransformation +from .resonance import _clar_optimization, _clar_transformation class ResonanceTest(unittest.TestCase): def testAllylShift(self): """Test allyl shift for hexadienyl radical""" - molList = generateResonanceStructures(Molecule(SMILES="C=C[CH]C=CC")) + molList = generate_resonance_structures(Molecule(SMILES="C=C[CH]C=CC")) self.assertEqual(len(molList), 3) def testOxime(self): """Test resonance structure generation for CC=N[O] radical Simple case for lone pair <=> radical resonance""" - molList = generateResonanceStructures(Molecule(SMILES="CC=N[O]")) + molList = generate_resonance_structures(Molecule(SMILES="CC=N[O]")) self.assertEqual(len(molList), 3) self.assertTrue(any([any([atom.charge != 0 for atom in mol.vertices]) for mol in molList])) @@ -52,7 +52,7 @@ def testAzide(self): """Test resonance structure generation for ethyl azide Simple case for N5dd <=> N5t resonance""" - molList = generateResonanceStructures(Molecule(SMILES="CCN=[N+]=[N-]")) + molList = generate_resonance_structures(Molecule(SMILES="CCN=[N+]=[N-]")) self.assertEqual(len(molList), 3) self.assertTrue(all([any([atom.charge != 0 for atom in mol.vertices]) for mol in molList])) @@ -60,139 +60,139 @@ def testStyryl1(self): """Test resonance structure generation for styryl, with radical on branch In this case, the radical can be delocalized into the aromatic ring""" - molList = generateResonanceStructures(Molecule(SMILES="c1ccccc1[C]=C")) + molList = generate_resonance_structures(Molecule(SMILES="c1ccccc1[C]=C")) self.assertEqual(len(molList), 4) def testStyryl2(self): """Test resonance structure generation for styryl, with radical on ring In this case, the radical can be delocalized into the aromatic ring""" - molList = generateResonanceStructures(Molecule(SMILES="C=C=C1C=C[CH]C=C1")) + molList = generate_resonance_structures(Molecule(SMILES="C=C=C1C=C[CH]C=C1")) self.assertEqual(len(molList), 4) def testNaphthyl(self): """Test resonance structure generation for naphthyl radical In this case, the radical is orthogonal to the pi-orbital plane and cannot delocalize""" - molList = generateResonanceStructures(Molecule(SMILES="c12[c]cccc1cccc2")) + molList = generate_resonance_structures(Molecule(SMILES="c12[c]cccc1cccc2")) self.assertEqual(len(molList), 4) def testMethylNapthalene(self): """Test resonance structure generation for methyl naphthalene Example of stable polycyclic aromatic species""" - molList = generateResonanceStructures(Molecule(SMILES="CC1=CC=CC2=CC=CC=C12")) + molList = generate_resonance_structures(Molecule(SMILES="CC1=CC=CC2=CC=CC=C12")) self.assertEqual(len(molList), 4) def testMethylPhenanthrene(self): """Test resonance structure generation for methyl phenanthrene Example of stable polycyclic aromatic species""" - molList = generateResonanceStructures(Molecule(SMILES="CC1=CC=CC2C3=CC=CC=C3C=CC=21")) + molList = generate_resonance_structures(Molecule(SMILES="CC1=CC=CC2C3=CC=CC=C3C=CC=21")) self.assertEqual(len(molList), 3) def testMethylPhenanthreneRadical(self): """Test resonance structure generation for methyl phenanthrene radical Example radical polycyclic aromatic species where the radical can delocalize""" - molList = generateResonanceStructures(Molecule(SMILES="[CH2]C1=CC=CC2C3=CC=CC=C3C=CC=21")) + molList = generate_resonance_structures(Molecule(SMILES="[CH2]C1=CC=CC2C3=CC=CC=C3C=CC=21")) self.assertEqual(len(molList), 9) def testAromaticWithLonePairResonance(self): """Test resonance structure generation for aromatic species with lone pair <=> radical resonance""" - molList = generateResonanceStructures(Molecule(SMILES="c1ccccc1CC=N[O]")) + molList = generate_resonance_structures(Molecule(SMILES="c1ccccc1CC=N[O]")) self.assertEqual(len(molList), 6) def testAromaticWithNResonance(self): """Test resonance structure generation for aromatic species with N5dd <=> N5t resonance""" - molList = generateResonanceStructures(Molecule(SMILES="c1ccccc1CCN=[N+]=[N-]")) + molList = generate_resonance_structures(Molecule(SMILES="c1ccccc1CCN=[N+]=[N-]")) self.assertEqual(len(molList), 6) def testNoClarStructures(self): """Test that we can turn off Clar structure generation.""" - molList = generateResonanceStructures(Molecule(SMILES='C1=CC=CC2C3=CC=CC=C3C=CC=21'), clarStructures=False) + molList = generate_resonance_structures(Molecule(SMILES='C1=CC=CC2C3=CC=CC=C3C=CC=21'), clarStructures=False) self.assertEqual(len(molList), 2) def testC13H11Rad(self): """Test resonance structure generation for p-methylbenzylbenzene radical Has multiple resonance structures that break aromaticity of a ring""" - molList = generateResonanceStructures(Molecule(SMILES="[CH](c1ccccc1)c1ccc(C)cc1")) + molList = generate_resonance_structures(Molecule(SMILES="[CH](c1ccccc1)c1ccc(C)cc1")) self.assertEqual(len(molList), 6) def testC8H8(self): """Test resonance structure generation for 5,6-dimethylene-1,3-cyclohexadiene Example of molecule that RDKit considers aromatic, but RMG does not""" - molList = generateResonanceStructures(Molecule(SMILES="C=C1C=CC=CC1=C")) + molList = generate_resonance_structures(Molecule(SMILES="C=C1C=CC=CC1=C")) self.assertEqual(len(molList), 1) def testC8H7J(self): """Test resonance structure generation for 5,6-dimethylene-1,3-cyclohexadiene radical Example of molecule that RDKit considers aromatic, but RMG does not""" - molList = generateResonanceStructures(Molecule(SMILES="C=C1C=CC=CC1=[CH]")) + molList = generate_resonance_structures(Molecule(SMILES="C=C1C=CC=CC1=[CH]")) self.assertEqual(len(molList), 1) def testC8H7J2(self): """Test resonance structure generation for 5,6-dimethylene-1,3-cyclohexadiene radical Example of molecule that RDKit considers aromatic, but RMG does not""" - molList = generateResonanceStructures(Molecule(SMILES="C=C1C=[C]C=CC1=C")) + molList = generate_resonance_structures(Molecule(SMILES="C=C1C=[C]C=CC1=C")) self.assertEqual(len(molList), 1) def test_C9H9_aro(self): """Test cyclopropyl benzene radical, aromatic SMILES""" mol = Molecule(SMILES="[CH]1CC1c1ccccc1") - molList = generateResonanceStructures(mol) + molList = generate_resonance_structures(mol) self.assertEqual(len(molList), 2) def test_C9H9_kek(self): """Test cyclopropyl benzene radical, kekulized SMILES""" mol = Molecule(SMILES="[CH]1CC1C1C=CC=CC=1") - molList = generateResonanceStructures(mol) + molList = generate_resonance_structures(mol) self.assertEqual(len(molList), 2) def test_Benzene_aro(self): """Test benzene, aromatic SMILES""" mol = Molecule(SMILES="c1ccccc1") - molList = generateResonanceStructures(mol) + molList = generate_resonance_structures(mol) self.assertEqual(len(molList), 2) def test_Benzene_kek(self): """Test benzene, kekulized SMILES""" mol = Molecule(SMILES="C1C=CC=CC=1") - molList = generateResonanceStructures(mol) + molList = generate_resonance_structures(mol) self.assertEqual(len(molList), 2) def test_C9H11_aro(self): """Test propylbenzene radical, aromatic SMILES""" mol = Molecule(SMILES="[CH2]CCc1ccccc1") - molList = generateResonanceStructures(mol) + molList = generate_resonance_structures(mol) self.assertEqual(len(molList), 2) def test_C10H11_aro(self): """Test cyclobutylbenzene radical, aromatic SMILES""" mol = Molecule(SMILES="[CH]1CCC1c1ccccc1") - molList = generateResonanceStructures(mol) + molList = generate_resonance_structures(mol) self.assertEqual(len(molList), 2) def test_C9H10_aro(self): """Test cyclopropylbenzene, aromatic SMILES""" mol = Molecule(SMILES="C1CC1c1ccccc1") - molList = generateResonanceStructures(mol) + molList = generate_resonance_structures(mol) self.assertEqual(len(molList), 2) def test_C10H12_aro(self): """Test cyclopropylmethyl benzene, aromatic SMILES""" mol = Molecule(SMILES="C1CC1c1c(C)cccc1") - molList = generateResonanceStructures(mol) + molList = generate_resonance_structures(mol) self.assertEqual(len(molList), 3) def test_C9H10_aro_2(self): """Test cyclopropyl benzene, generate aromatic resonance isomers""" mol = Molecule(SMILES="C1CC1c1ccccc1") - molList = generateAromaticResonanceStructures(mol) + molList = generate_aromatic_resonance_structures(mol) self.assertEqual(len(molList), 1) def testFusedAromatic1(self): @@ -232,7 +232,7 @@ def testFusedAromatic1(self): 32 H u0 p0 c0 {16,S} """) perylene2 = Molecule().fromSMILES('c1cc2cccc3c4cccc5cccc(c(c1)c23)c54') - for isomer in generateAromaticResonanceStructures(perylene2): + for isomer in generate_aromatic_resonance_structures(perylene2): if perylene.isIsomorphic(isomer): break else: # didn't break @@ -264,7 +264,7 @@ def testFusedAromatic2(self): 18 H u0 p0 c0 {6,S} """) naphthalene2 = Molecule().fromSMILES('C1=CC=C2C=CC=CC2=C1') - for isomer in generateAromaticResonanceStructures(naphthalene2): + for isomer in generate_aromatic_resonance_structures(naphthalene2): if naphthalene.isIsomorphic(isomer): break else: # didn't break @@ -274,7 +274,7 @@ def testFusedAromatic2(self): )) def testAromaticResonanceStructures(self): - """Test that generateAromaticResonanceStructures gives consistent output + """Test that generate_aromatic_resonance_structures gives consistent output Check that we get the same resonance structure regardless of which structure we start with""" # Kekulized form, radical on methyl @@ -367,9 +367,9 @@ def testAromaticResonanceStructures(self): 25 H u0 p0 c0 {15,S} 26 H u0 p0 c0 {15,S} """) - result1 = generateAromaticResonanceStructures(struct1) - result2 = generateAromaticResonanceStructures(struct2) - result3 = generateAromaticResonanceStructures(struct3) + result1 = generate_aromatic_resonance_structures(struct1) + result2 = generate_aromatic_resonance_structures(struct2) + result3 = generate_aromatic_resonance_structures(struct3) self.assertEqual(len(result1), 1) self.assertEqual(len(result2), 1) @@ -403,7 +403,7 @@ def testBridgedAromatic(self): 16 H u0 p0 c0 {8,S} """) - out = generateResonanceStructures(mol) + out = generate_resonance_structures(mol) self.assertEqual(len(out), 3) self.assertTrue(arom.isIsomorphic(out[1])) @@ -435,7 +435,7 @@ def testPolycyclicAromaticWithNonAromaticRing(self): 17 H u0 p0 c0 {9,S} """) - out = generateResonanceStructures(mol) + out = generate_resonance_structures(mol) self.assertEqual(len(out), 2) self.assertTrue(arom.isIsomorphic(out[1])) @@ -489,7 +489,7 @@ def testPolycyclicAromaticWithNonAromaticRing2(self): 40 H u0 p0 c0 {24,S} """) - out = generateResonanceStructures(mol) + out = generate_resonance_structures(mol) self.assertEqual(len(out), 4) self.assertTrue(arom.isIsomorphic(out[1])) @@ -524,7 +524,7 @@ def testKekulizeBenzene(self): 11 H u0 p0 c0 {5,S} 12 H u0 p0 c0 {6,S} """) - out = generateKekuleStructure(arom) + out = generate_kekule_structure(arom) self.assertEqual(len(out), 1) self.assertTrue(out[0].isIsomorphic(keku)) @@ -551,7 +551,7 @@ def testKekulizeNaphthalene(self): 17 H u0 p0 c0 {5,S} 18 H u0 p0 c0 {6,S} """) - out = generateKekuleStructure(arom) + out = generate_kekule_structure(arom) self.assertEqual(len(out), 1) self.assertFalse(out[0].isAromatic()) @@ -595,7 +595,7 @@ def testKekulizePhenanthrene(self): 23 H u0 p0 c0 {9,S} 24 H u0 p0 c0 {10,S} """) - out = generateKekuleStructure(arom) + out = generate_kekule_structure(arom) self.assertEqual(len(out), 1) self.assertFalse(out[0].isAromatic()) @@ -641,7 +641,7 @@ def testKekulizePyrene(self): 25 H u0 p0 c0 {13,S} 26 H u0 p0 c0 {14,S} """) - out = generateKekuleStructure(arom) + out = generate_kekule_structure(arom) self.assertEqual(len(out), 1) self.assertFalse(out[0].isAromatic()) @@ -691,7 +691,7 @@ def testKekulizeCorannulene(self): 29 H u0 p0 c0 {19,S} 30 H u0 p0 c0 {20,S} """) - out = generateKekuleStructure(arom) + out = generate_kekule_structure(arom) self.assertEqual(len(out), 1) self.assertFalse(out[0].isAromatic()) @@ -747,7 +747,7 @@ def testKekulizeCoronene(self): 35 H u0 p0 c0 {23,S} 36 H u0 p0 c0 {24,S} """) - out = generateKekuleStructure(arom) + out = generate_kekule_structure(arom) self.assertEqual(len(out), 1) self.assertFalse(out[0].isAromatic()) @@ -783,7 +783,7 @@ def testKekulizeBridgedAromatic(self): 15 H u0 p0 c0 {9,S} 16 H u0 p0 c0 {10,S} """) - out = generateKekuleStructure(arom) + out = generate_kekule_structure(arom) self.assertEqual(len(out), 1) self.assertFalse(out[0].isAromatic()) @@ -840,10 +840,10 @@ def testKekulizeResonanceIsomer(self): 14 H u0 p0 c0 {7,S} 15 H u0 p0 c0 {7,S} """) - kekulized_isomer = generateKekuleStructure(toluene)[0] + kekulized_isomer = generate_kekule_structure(toluene)[0] self.assertTrue(kekulized_isomer.isIsomorphic(toluene_kekulized)) - for isomer in generateResonanceStructures(toluene): + for isomer in generate_resonance_structures(toluene): if isomer.isIsomorphic(toluene_kekulized): break else: # didn't brake @@ -872,7 +872,7 @@ def testMultipleKekulizedResonanceIsomers(self): """ molecule = Molecule().fromAdjacencyList(adjlist_aromatic) self.assertTrue(molecule.isAromatic(), "Starting molecule should be aromatic") - isomers = generateResonanceStructures(molecule) + isomers = generate_resonance_structures(molecule) self.assertEqual(len(isomers), 3, "Didn't generate 3 resonance isomers") self.assertFalse(isomers[1].isAromatic(), "Second resonance isomer shouldn't be aromatic") self.assertFalse(isomers[2].isAromatic(), "Third resonance isomer shouldn't be aromatic") @@ -900,7 +900,7 @@ def testMultipleKekulizedResonanceIsomersRad(self): """ molecule = Molecule().fromAdjacencyList(adjlist_aromatic) self.assertTrue(molecule.isAromatic(), "Starting molecule should be aromatic") - molList = generateResonanceStructures(molecule) + molList = generate_resonance_structures(molecule) self.assertEqual(len(molList), 6, "Expected 6 resonance structures, but generated {0}.".format(len(molList))) aromatic = 0 for mol in molList: @@ -1043,7 +1043,7 @@ def testKekulizedResonanceIsomersFused(self): for starting in resonance_forms: self.assertFalse(starting.isAromatic(), "Starting molecule should not be aromatic") - isomers = generateResonanceStructures(starting) + isomers = generate_resonance_structures(starting) # print "starting with {0!r} I generated these:".format(starting) # print repr(isomers) for isomer in isomers: @@ -1073,7 +1073,7 @@ def testKeepIsomorphicStructuresFunctionsWhenTrue(self): """Test that keepIsomorphic works for resonance structure generation when True.""" mol = Molecule(SMILES='C=C[CH2]') mol.assignAtomIDs() - out = mol.generateResonanceIsomers(keepIsomorphic=True) + out = generate_resonance_structures(mol, keepIsomorphic=True) self.assertEqual(len(out), 2) self.assertTrue(out[0].isIsomorphic(out[1])) @@ -1083,10 +1083,108 @@ def testKeepIsomorphicStructuresFunctionsWhenFalse(self): """Test that keepIsomorphic works for resonance structure generation when False.""" mol = Molecule(SMILES='C=C[CH2]') mol.assignAtomIDs() - out = mol.generateResonanceIsomers(keepIsomorphic=False) + out = generate_resonance_structures(mol, keepIsomorphic=False) self.assertEqual(len(out), 1) + def testFalseNegativeAromaticityPerception(self): + """Test that we obtain the correct aromatic structure for a monocyclic aromatic that RDKit mis-identifies.""" + mol = Molecule(SMILES='[CH2]C=C1C=CC(=C)C=C1') + out = generate_resonance_structures(mol) + + aromatic = Molecule().fromAdjacencyList(""" +multiplicity 2 +1 C u0 p0 c0 {4,B} {5,B} {7,S} +2 C u0 p0 c0 {3,B} {6,B} {8,S} +3 C u0 p0 c0 {2,B} {4,B} {10,S} +4 C u0 p0 c0 {1,B} {3,B} {11,S} +5 C u0 p0 c0 {1,B} {6,B} {13,S} +6 C u0 p0 c0 {2,B} {5,B} {14,S} +7 C u0 p0 c0 {1,S} {9,D} {12,S} +8 C u1 p0 c0 {2,S} {15,S} {16,S} +9 C u0 p0 c0 {7,D} {17,S} {18,S} +10 H u0 p0 c0 {3,S} +11 H u0 p0 c0 {4,S} +12 H u0 p0 c0 {7,S} +13 H u0 p0 c0 {5,S} +14 H u0 p0 c0 {6,S} +15 H u0 p0 c0 {8,S} +16 H u0 p0 c0 {8,S} +17 H u0 p0 c0 {9,S} +18 H u0 p0 c0 {9,S} +""") + + self.assertEqual(len(out), 5) + self.assertTrue(any([m.isIsomorphic(aromatic) for m in out])) + + def testFalseNegativePolycyclicAromaticityPerception(self): + """Test that we generate proper structures for a polycyclic aromatic that RDKit mis-identifies.""" + mol = Molecule(SMILES='C=C1C=CC=C2C=C[CH]C=C12') + out = generate_resonance_structures(mol) + + clar = Molecule().fromAdjacencyList(""" +multiplicity 2 +1 C u0 p0 c0 {2,B} {3,B} {7,S} +2 C u0 p0 c0 {1,B} {5,B} {6,S} +3 C u0 p0 c0 {1,B} {4,B} {11,S} +4 C u0 p0 c0 {3,B} {8,B} {13,S} +5 C u0 p0 c0 {2,B} {8,B} {15,S} +6 C u0 p0 c0 {2,S} {9,D} {16,S} +7 C u0 p0 c0 {1,S} {10,D} {18,S} +8 C u0 p0 c0 {4,B} {5,B} {14,S} +9 C u0 p0 c0 {6,D} {10,S} {17,S} +10 C u0 p0 c0 {7,D} {9,S} {12,S} +11 C u1 p0 c0 {3,S} {19,S} {20,S} +12 H u0 p0 c0 {10,S} +13 H u0 p0 c0 {4,S} +14 H u0 p0 c0 {8,S} +15 H u0 p0 c0 {5,S} +16 H u0 p0 c0 {6,S} +17 H u0 p0 c0 {9,S} +18 H u0 p0 c0 {7,S} +19 H u0 p0 c0 {11,S} +20 H u0 p0 c0 {11,S} +""") + + self.assertEqual(len(out), 6) + self.assertTrue(any([m.isIsomorphic(clar) for m in out])) + + def testFalseNegativePolycylicAromaticityPerception2(self): + """Test that we obtain the correct aromatic structure for a polycylic aromatic that RDKit mis-identifies.""" + mol = Molecule(SMILES='[CH2]C=C1C=CC(=C)C2=C1C=CC=C2') + out = generate_resonance_structures(mol) + + aromatic = Molecule().fromAdjacencyList(""" +multiplicity 2 +1 C u0 p0 c0 {2,B} {4,B} {8,B} +2 C u0 p0 c0 {1,B} {3,B} {7,B} +3 C u0 p0 c0 {2,B} {5,B} {9,S} +4 C u0 p0 c0 {1,B} {6,B} {12,S} +5 C u0 p0 c0 {3,B} {6,B} {15,S} +6 C u0 p0 c0 {4,B} {5,B} {16,S} +7 C u0 p0 c0 {2,B} {10,B} {17,S} +8 C u0 p0 c0 {1,B} {11,B} {20,S} +9 C u0 p0 c0 {3,S} {13,D} {14,S} +10 C u0 p0 c0 {7,B} {11,B} {18,S} +11 C u0 p0 c0 {8,B} {10,B} {19,S} +12 C u1 p0 c0 {4,S} {21,S} {22,S} +13 C u0 p0 c0 {9,D} {23,S} {24,S} +14 H u0 p0 c0 {9,S} +15 H u0 p0 c0 {5,S} +16 H u0 p0 c0 {6,S} +17 H u0 p0 c0 {7,S} +18 H u0 p0 c0 {10,S} +19 H u0 p0 c0 {11,S} +20 H u0 p0 c0 {8,S} +21 H u0 p0 c0 {12,S} +22 H u0 p0 c0 {12,S} +23 H u0 p0 c0 {13,S} +24 H u0 p0 c0 {13,S} +""") + + self.assertEqual(len(out), 7) + self.assertTrue(any([m.isIsomorphic(aromatic) for m in out])) + class ClarTest(unittest.TestCase): """ @@ -1097,7 +1195,7 @@ def testClarTransformation(self): """Test that clarTransformation generates an aromatic ring.""" mol = Molecule().fromSMILES('c1ccccc1') sssr = mol.getSmallestSetOfSmallestRings() - _clarTransformation(mol, sssr[0]) + _clar_transformation(mol, sssr[0]) mol.updateAtomTypes() self.assertTrue(mol.isAromatic()) @@ -1105,7 +1203,7 @@ def testClarTransformation(self): def testClarOptimization(self): """Test to ensure pi electrons are conserved during optimization""" mol = Molecule().fromSMILES('C1=CC=C2C=CC=CC2=C1') # Naphthalene - output = _clarOptimization(mol) + output = _clar_optimization(mol) for molecule, asssr, bonds, solution in output: @@ -1130,7 +1228,7 @@ def testClarOptimization(self): def testPhenanthrene(self): """Test that we generate 1 Clar structure for phenanthrene.""" mol = Molecule().fromSMILES('C1=CC=C2C(C=CC3=CC=CC=C32)=C1') - newmol = generateClarStructures(mol) + newmol = generate_clar_structures(mol) struct = Molecule().fromAdjacencyList("""1 C u0 p0 c0 {2,S} {3,B} {5,B} 2 C u0 p0 c0 {1,S} {4,B} {9,B} @@ -1166,7 +1264,7 @@ def testPhenalene(self): Case where there is one non-aromatic ring.""" mol = Molecule().fromSMILES('C1=CC2=CC=CC3CC=CC(=C1)C=32') - newmol = generateClarStructures(mol) + newmol = generate_clar_structures(mol) struct1 = Molecule().fromAdjacencyList("""1 C u0 p0 c0 {2,S} {6,S} {14,S} {15,S} 2 C u0 p0 c0 {1,S} {3,S} {7,D} @@ -1227,7 +1325,7 @@ def testCorannulene(self): Case where linear relaxation does not give an integer solution""" mol = Molecule().fromSMILES('C1=CC2=CC=C3C=CC4=C5C6=C(C2=C35)C1=CC=C6C=C4') - newmol = generateClarStructures(mol) + newmol = generate_clar_structures(mol) struct = Molecule().fromAdjacencyList("""1 C u0 p0 c0 {2,S} {5,B} {8,B} 2 C u0 p0 c0 {1,S} {3,B} {10,B} @@ -1275,6 +1373,6 @@ def testExocyclicDB(self): from exocyclic double bonds, while they don't actually contribute to aromaticity""" mol = Molecule(SMILES="C=C1C=CC=CC1=C") - newmol = generateClarStructures(mol) + newmol = generate_clar_structures(mol) self.assertEquals(len(newmol), 0) diff --git a/rmgpy/molecule/symmetryTest.py b/rmgpy/molecule/symmetryTest.py index 9b6c78ca82..a9ad276b85 100644 --- a/rmgpy/molecule/symmetryTest.py +++ b/rmgpy/molecule/symmetryTest.py @@ -34,7 +34,7 @@ from rmgpy.molecule.molecule import Molecule from rmgpy.molecule.symmetry import calculateAtomSymmetryNumber, calculateAxisSymmetryNumber, calculateBondSymmetryNumber, calculateCyclicSymmetryNumber from rmgpy.species import Species -from rmgpy.molecule.resonance import generateAromaticResonanceStructures +from rmgpy.molecule.resonance import generate_aromatic_resonance_structures ################################################################################ class TestMoleculeSymmetry(unittest.TestCase): @@ -531,7 +531,7 @@ def testTotalSymmetryNumberPhenoxyBenzene(self): """ molecule = Molecule().fromSMILES('c1ccccc1[O]') species = Species(molecule=[molecule]) - aromatic_molecule = generateAromaticResonanceStructures(molecule)[0] + aromatic_molecule = generate_aromatic_resonance_structures(molecule)[0] symmetryNumber = aromatic_molecule.getSymmetryNumber() self.assertEqual(symmetryNumber, 2) diff --git a/rmgpy/reaction.pxd b/rmgpy/reaction.pxd index 242e69f019..ff9f1a79bc 100644 --- a/rmgpy/reaction.pxd +++ b/rmgpy/reaction.pxd @@ -61,7 +61,7 @@ cdef class Reaction: cpdef bint hasTemplate(self, list reactants, list products) - cpdef bint matchesMolecules(self, list reactants) + cpdef bint matchesSpecies(self, list reactants, list products=?) cpdef bint isIsomorphic(self, Reaction other, bint eitherDirection=?, bint checkIdentical=?, bint checkOnlyLabel=?, bint checkTemplateRxnProducts=?) diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index ae892f1d1e..6b882cd558 100755 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -360,35 +360,28 @@ def hasTemplate(self, reactants, products): (all([spec in self.products for spec in reactants]) and all([spec in self.reactants for spec in products]))) - def matchesMolecules(self, reactants): + def matchesSpecies(self, reactants, products=None): """ - Return ``True`` if the given ``reactants`` represent the total set of - reactants or products for the current ``reaction``, or ``False`` if not. - The reactants should be :class:`Molecule` objects. + Compares the provided reactants and products against the reactants + and products of this reaction. Both directions are checked. + + Args: + reactants List of Species required on one side of the reaction + products List of Species required on the other side (optional) """ - assert all([isinstance(reactant, Molecule) for reactant in reactants]) # Check forward direction - if len(reactants) == len(self.reactants) == 1: - if self.reactants[0].isIsomorphic(reactants[0]): - return True - elif len(reactants) == len(self.reactants) == 2: - if self.reactants[0].isIsomorphic(reactants[0]) and self.reactants[1].isIsomorphic(reactants[1]): - return True - elif self.reactants[0].isIsomorphic(reactants[1]) and self.reactants[1].isIsomorphic(reactants[0]): + if _isomorphicSpeciesList(self.reactants, reactants): + if products is None or _isomorphicSpeciesList(self.products, products): return True - # Check reverse direction - if len(reactants) == len(self.products) == 1: - if self.products[0].isIsomorphic(reactants[0]): - return True - elif len(reactants) == len(self.products) == 2: - if self.products[0].isIsomorphic(reactants[0]) and self.products[1].isIsomorphic(reactants[1]): - return True - elif self.products[0].isIsomorphic(reactants[1]) and self.products[1].isIsomorphic(reactants[0]): + else: + return False + elif _isomorphicSpeciesList(self.products, reactants): + if products is None or _isomorphicSpeciesList(self.reactants, products): return True - if len(reactants) > 2: - raise NotImplementedError("Can't check isomorphism of reactions with {0} reactants".format(len(reactants))) - # If we're here then neither direction matched, so return false - return False + else: + return False + else: + return False def isIsomorphic(self, other, eitherDirection=True, checkIdentical = False, checkOnlyLabel = False, checkTemplateRxnProducts=False): diff --git a/rmgpy/reduction/reduction.py b/rmgpy/reduction/reduction.py index 6b85d8f2d3..0d486ec956 100644 --- a/rmgpy/reduction/reduction.py +++ b/rmgpy/reduction/reduction.py @@ -87,7 +87,7 @@ def simulateOne(reactionModel, atol, rtol, reactionSystem): simulatorSettings = SimulatorSettings(atol,rtol) modelSettings = ModelSettings(toleranceKeepInEdge=0,toleranceMoveToCore=1,toleranceInterruptSimulation=1) - terminated, obj,sspcs,srxns = reactionSystem.simulate( + terminated,resurrected,obj,sspcs,srxns = reactionSystem.simulate( coreSpecies = reactionModel.core.species, coreReactions = reactionModel.core.reactions, edgeSpecies = reactionModel.edge.species, diff --git a/rmgpy/rmg/input.py b/rmgpy/rmg/input.py index be22b44251..3b45920b8f 100644 --- a/rmgpy/rmg/input.py +++ b/rmgpy/rmg/input.py @@ -234,7 +234,7 @@ def model(toleranceMoveToCore=None, toleranceMoveEdgeReactionToCore=numpy.inf,to toleranceMoveEdgeReactionToSurfaceInterrupt=None, toleranceMoveEdgeReactionToCoreInterrupt=None, maximumEdgeSpecies=1000000, minCoreSizeForPrune=50, minSpeciesExistIterationsForPrune=2, filterReactions=False, ignoreOverallFluxCriterion=False, - maxNumSpecies=None,maxNumObjsPerIter=1,terminateAtMaxObjects=False,toleranceThermoKeepSpeciesInEdge=numpy.inf): + maxNumSpecies=None,maxNumObjsPerIter=1,terminateAtMaxObjects=False,toleranceThermoKeepSpeciesInEdge=numpy.inf,dynamicsTimeScale=(0.0,'sec')): """ How to generate the model. `toleranceMoveToCore` must be specified. toleranceMoveReactionToCore and toleranceReactionInterruptSimulation refers to an additional criterion for forcing an edge reaction to be included in the core @@ -251,7 +251,8 @@ def model(toleranceMoveToCore=None, toleranceMoveEdgeReactionToCore=numpy.inf,to rmg.modelSettingsList.append(ModelSettings(toleranceMoveToCore, toleranceMoveEdgeReactionToCore,toleranceKeepInEdge, toleranceInterruptSimulation, toleranceMoveEdgeReactionToSurface, toleranceMoveSurfaceSpeciesToCore, toleranceMoveSurfaceReactionToCore, toleranceMoveEdgeReactionToSurfaceInterrupt,toleranceMoveEdgeReactionToCoreInterrupt, maximumEdgeSpecies, minCoreSizeForPrune, - minSpeciesExistIterationsForPrune, filterReactions, ignoreOverallFluxCriterion, maxNumSpecies, maxNumObjsPerIter,terminateAtMaxObjects,toleranceThermoKeepSpeciesInEdge)) + minSpeciesExistIterationsForPrune, filterReactions, ignoreOverallFluxCriterion, maxNumSpecies, maxNumObjsPerIter,terminateAtMaxObjects, + toleranceThermoKeepSpeciesInEdge,Quantity(dynamicsTimeScale))) def quantumMechanics( software, diff --git a/rmgpy/rmg/main.py b/rmgpy/rmg/main.py index 8be695a30c..65daf9806d 100644 --- a/rmgpy/rmg/main.py +++ b/rmgpy/rmg/main.py @@ -211,6 +211,8 @@ def loadInput(self, path=None): if self.pressureDependence: self.pressureDependence.outputFile = self.outputDirectory self.reactionModel.pressureDependence = self.pressureDependence + if self.solvent: + self.reactionModel.solventName = self.solvent self.reactionModel.verboseComments = self.verboseComments self.reactionModel.saveEdgeSpecies = self.saveEdgeSpecies @@ -393,10 +395,8 @@ def initialize(self, **kwargs): # Do all liquid-phase startup things: if self.solvent: - Species.solventData = self.database.solvation.getSolventData(self.solvent) - Species.solventName = self.solvent - Species.solventStructure = self.database.solvation.getSolventStructure(self.solvent) - diffusionLimiter.enable(Species.solventData, self.database.solvation) + solventData = self.database.solvation.getSolventData(self.solvent) + diffusionLimiter.enable(solventData, self.database.solvation) logging.info("Setting solvent data for {0}".format(self.solvent)) try: @@ -447,7 +447,8 @@ def initialize(self, **kwargs): # For liquidReactor, checks whether the solvent is listed as one of the initial species. if self.solvent: - self.database.solvation.checkSolventinInitialSpecies(self,Species.solventStructure) + solventStructure = self.database.solvation.getSolventStructure(self.solvent) + self.database.solvation.checkSolventinInitialSpecies(self,solventStructure) #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 @@ -470,7 +471,7 @@ def initialize(self, **kwargs): """.format(spec.label)) for spec in self.initialSpecies: - submit(spec) + submit(spec,self.solvent) # Add nonreactive species (e.g. bath gases) to core first # This is necessary so that the PDep algorithm can identify the bath gas @@ -597,6 +598,8 @@ def execute(self, **kwargs): allTerminated = True numCoreSpecies = len(self.reactionModel.core.species) + + notResurrectedVec = [True for i in xrange(len(self.reactionSystems))] for index, reactionSystem in enumerate(self.reactionSystems): self.reactionSystem = reactionSystem @@ -610,7 +613,7 @@ def execute(self, **kwargs): # Turn pruning off if we haven't reached minimum core size. prune = False - try: terminated, obj,newSurfaceSpecies,newSurfaceReactions = reactionSystem.simulate( + try: terminated,resurrected,obj,newSurfaceSpecies,newSurfaceReactions = reactionSystem.simulate( coreSpecies = self.reactionModel.core.species, coreReactions = self.reactionModel.core.reactions, edgeSpecies = self.reactionModel.edge.species, @@ -631,7 +634,9 @@ def execute(self, **kwargs): logging.error(prettify(repr(self.reactionModel.core.reactions))) self.makeSeedMech() raise - + + notResurrectedVec[index] = not resurrected + if self.generateSeedEachIteration: self.makeSeedMech() @@ -684,20 +689,23 @@ def execute(self, **kwargs): # Run a raw simulation to get updated reaction system threshold values for index, reactionSystem in enumerate(self.reactionSystems): # Run with the same conditions as with pruning off - reactionSystem.simulate( - coreSpecies = self.reactionModel.core.species, - coreReactions = self.reactionModel.core.reactions, - edgeSpecies = [], - edgeReactions = [], - surfaceSpecies = self.reactionModel.surface.species, - surfaceReactions = self.reactionModel.surface.reactions, - pdepNetworks = self.reactionModel.networkList, - modelSettings = tempModelSettings, - simulatorSettings = simulatorSettings, - ) - self.updateReactionThresholdAndReactFlags( - rxnSysUnimolecularThreshold = reactionSystem.unimolecularThreshold, - rxnSysBimolecularThreshold = reactionSystem.bimolecularThreshold) + if notResurrectedVec[index]: + reactionSystem.simulate( + coreSpecies = self.reactionModel.core.species, + coreReactions = self.reactionModel.core.reactions, + edgeSpecies = [], + edgeReactions = [], + surfaceSpecies = self.reactionModel.surface.species, + surfaceReactions = self.reactionModel.surface.reactions, + pdepNetworks = self.reactionModel.networkList, + modelSettings = tempModelSettings, + simulatorSettings = simulatorSettings, + ) + self.updateReactionThresholdAndReactFlags( + rxnSysUnimolecularThreshold = reactionSystem.unimolecularThreshold, + rxnSysBimolecularThreshold = reactionSystem.bimolecularThreshold) + else: + logging.warn('Reaction thresholds/flags for Reaction System {0} was not updated due to resurrection'.format(index+1)) logging.info('') else: @@ -755,7 +763,7 @@ def execute(self, **kwargs): csvfilePath = os.path.join(self.outputDirectory, 'solver', 'sensitivity_{0}_SPC_{1}.csv'.format(index+1, spec.index)) sensWorksheet.append(csvfilePath) - terminated, obj, surfaceSpecies, surfaceReactions = reactionSystem.simulate( + terminated, resurrected,obj, surfaceSpecies, surfaceReactions = reactionSystem.simulate( coreSpecies = self.reactionModel.core.species, coreReactions = self.reactionModel.core.reactions, edgeSpecies = self.reactionModel.edge.species, diff --git a/rmgpy/rmg/model.py b/rmgpy/rmg/model.py index aafd728a08..50d1c88f31 100644 --- a/rmgpy/rmg/model.py +++ b/rmgpy/rmg/model.py @@ -43,7 +43,7 @@ import rmgpy.constants as constants from rmgpy.constraints import failsSpeciesConstraints from rmgpy.quantity import Quantity -import rmgpy.species +from rmgpy.species import Species from rmgpy.thermo.thermoengine import submit from rmgpy.reaction import Reaction from rmgpy.exceptions import ForbiddenStructureException @@ -56,31 +56,8 @@ from .react import reactAll from pdep import PDepReaction, PDepNetwork -# generateThermoDataFromQM under the Species class imports the qm package - -################################################################################ - -class Species(rmgpy.species.Species): - solventName = None - solventData = None - # solventStructure is the instance of species class whose molecule attribute corresponds to the solvent SMILES. - # If the solvent library does not contain the SMILES of the solvent, then the solventStructure is None - solventStructure = None - solventViscosity = None - isSolvent = False # returns True if the species is the solvent and False if not - diffusionTemp = None - - def __init__(self, index=-1, label='', thermo=None, conformer=None, - molecule=None, transportData=None, molecularWeight=None, - energyTransferModel=None, reactive=True, props=None, creationIteration=0): - rmgpy.species.Species.__init__(self, index, label, thermo, conformer, molecule, transportData, molecularWeight, energyTransferModel, reactive, props) - self.creationIteration = creationIteration - def __reduce__(self): - """ - A helper function used when pickling an object. - """ - return (Species, (self.index, self.label, self.thermo, self.conformer, self.molecule, self.transportData, self.molecularWeight, self.energyTransferModel, self.reactive, self.props,self.creationIteration),) +# generateThermoDataFromQM under the Species class imports the qm package ################################################################################ @@ -196,6 +173,7 @@ class CoreEdgeReactionModel: `networkList` A list of pressure-dependent reaction networks (:class:`Network` objects) `networkCount` A counter for the number of pressure-dependent networks created `indexSpeciesDict` A dictionary with a unique index pointing to the species objects + `solventName` String describing solvent name for liquid reactions. Empty for non-liquid estimation ========================= ============================================================== @@ -249,7 +227,8 @@ def __init__(self, core=None, edge=None, surface=None): self.newSurfaceRxnsAdd = set() self.newSurfaceSpcsLoss = set() self.newSurfaceRxnsLoss = set() - + self.solventName = '' + def checkForExistingSpecies(self, molecule): """ Check to see if an existing species contains the same @@ -271,8 +250,8 @@ def checkForExistingSpecies(self, molecule): # within the list of isomers for a species object describing a unique aromatic compound if molecule.isCyclic(): obj = Species(molecule=[molecule]) - from rmgpy.molecule.resonance import generateAromaticResonanceStructures - aromaticIsomers = generateAromaticResonanceStructures(molecule) + from rmgpy.molecule.resonance import generate_aromatic_resonance_structures + aromaticIsomers = generate_aromatic_resonance_structures(molecule) obj.molecule.extend(aromaticIsomers) # First check cache and return if species is found @@ -335,10 +314,10 @@ def makeNewSpecies(self, object, label='', reactive=True, checkForExisting=True) spec = Species(index=speciesIndex, label=label, molecule=[molecule], reactive=reactive) spec.creationIteration = self.iterationNum - spec.generateResonanceIsomers() + spec.generate_resonance_structures() spec.molecularWeight = Quantity(spec.molecule[0].getMolecularWeight()*1000.,"amu") - submit(spec) + submit(spec,self.solventName) if spec.label == '': if spec.thermo and spec.thermo.label != '': #check if thermo libraries have a name for it @@ -489,7 +468,8 @@ def makeNewReaction(self, forward, checkExisting=True): # Determine the proper species objects for all reactants and products reactants = [self.makeNewSpecies(reactant)[0] for reactant in forward.reactants] products = [self.makeNewSpecies(product)[0] for product in forward.products ] - if forward.specificCollider is not None: forward.specificCollider = self.makeNewSpecies(forward.specificCollider)[0] + if forward.specificCollider is not None: + forward.specificCollider = self.makeNewSpecies(forward.specificCollider)[0] if forward.pairs is not None: for pairIndex in range(len(forward.pairs)): @@ -1448,7 +1428,7 @@ def addSeedMechanismToCore(self, seedMechanism, react=False): for spec in self.newSpeciesList: if spec.reactive: - submit(spec) + submit(spec,self.solventName) self.addSpeciesToCore(spec) @@ -1458,7 +1438,7 @@ def addSeedMechanismToCore(self, seedMechanism, react=False): # we need to make sure the barrier is positive. # ...but are Seed Mechanisms run through PDep? Perhaps not. for spec in itertools.chain(rxn.reactants, rxn.products): - submit(spec) + submit(spec,self.solventName) rxn.fixBarrierHeight(forcePositive=True) self.addReactionToCore(rxn) @@ -1514,7 +1494,7 @@ def addReactionLibraryToEdge(self, reactionLibrary): for spec in self.newSpeciesList: if spec.reactive: - submit(spec) + submit(spec,self.solventName) self.addSpeciesToEdge(spec) @@ -1951,7 +1931,8 @@ def areIdenticalSpeciesReferences(rxn1, rxn2): Checks if the references of the reactants and products of the two reactions are identical, in either direction. """ - - return any([rxn1.reactants == rxn2.reactants and rxn1.products == rxn2.products, \ - rxn1.reactants == rxn2.products and rxn1.products == rxn2.reactants - ]) + identical_same_direction = rxn1.reactants == rxn2.reactants and rxn1.products == rxn2.products + identical_opposite_directions = rxn1.reactants == rxn2.products and rxn1.products == rxn2.reactants + identical_collider = rxn1.specificCollider == rxn2.specificCollider + + return (identical_same_direction or identical_opposite_directions) and identical_collider diff --git a/rmgpy/rmg/modelTest.py b/rmgpy/rmg/modelTest.py index 449fb3da81..380d5f60b7 100644 --- a/rmgpy/rmg/modelTest.py +++ b/rmgpy/rmg/modelTest.py @@ -409,7 +409,7 @@ def test_checkForExistingReaction_elminates_duplicate(self): spcB.label = 'C=C[CH2]C' spcC.label = 'C=C=CC' spcD.label = '[H][H]' - spcB.generateResonanceIsomers() + spcB.generate_resonance_structures() cerm.addSpeciesToCore(spcA) cerm.addSpeciesToCore(spcB) @@ -450,7 +450,7 @@ def test_checkForExistingReaction_keeps_different_template_reactions(self): spcB.label = 'C=C[CH2]C' spcC.label = 'C=C=CC' spcD.label = '[H][H]' - spcB.generateResonanceIsomers() + spcB.generate_resonance_structures() cerm.addSpeciesToCore(spcA) cerm.addSpeciesToCore(spcB) @@ -492,7 +492,7 @@ def test_checkForExistingReaction_removes_duplicates_in_opposite_directions(self spcB.label = 'C=C[CH2]C' spcC.label = 'C=C=CC' spcD.label = '[H][H]' - spcB.generateResonanceIsomers() + spcB.generate_resonance_structures() cerm.addSpeciesToCore(spcA) cerm.addSpeciesToCore(spcB) @@ -533,7 +533,7 @@ def test_checkForExistingReaction_keeps_different_template_reactions(self): spcB.label = 'C=C[CH2]C' spcC.label = 'C=C=CC' spcD.label = '[H][H]' - spcB.generateResonanceIsomers() + spcB.generate_resonance_structures() cerm.addSpeciesToCore(spcA) cerm.addSpeciesToCore(spcB) diff --git a/rmgpy/rmg/pdep.py b/rmgpy/rmg/pdep.py index d0f93a71dd..ec99fa10b9 100644 --- a/rmgpy/rmg/pdep.py +++ b/rmgpy/rmg/pdep.py @@ -540,9 +540,8 @@ def update(self, reactionModel, pdepSettings): bathGas = [spec for spec in reactionModel.core.species if not spec.reactive] self.bathGas = {} for spec in bathGas: - # is this really the only/best way to weight them? And what is alpha0? + # is this really the only/best way to weight them? self.bathGas[spec] = 1.0 / len(bathGas) - spec.collisionModel = SingleExponentialDown(alpha0=(4.86,'kcal/mol')) # Save input file if not self.label: self.label = str(self.index) diff --git a/rmgpy/rmg/react.py b/rmgpy/rmg/react.py index b2786575d5..492d01d2b0 100644 --- a/rmgpy/rmg/react.py +++ b/rmgpy/rmg/react.py @@ -31,13 +31,12 @@ """ Contains functions for generating reactions. """ -import logging import itertools from rmgpy.data.rmg import getDB from rmgpy.scoop_framework.util import map_ -from rmgpy.species import Species - + + def react(*spcTuples): """ Generate reactions between the species in the @@ -65,42 +64,14 @@ def react(*spcTuples): def reactSpecies(speciesTuple): """ - given one species tuple, will find the reactions and remove degeneracy - from them. - """ - # Check if the reactants are the same - sameReactants = False - if len(speciesTuple) == 2 and speciesTuple[0].isIsomorphic(speciesTuple[1]): - sameReactants = True + Given a tuple of Species objects, generates all possible reactions + from the loaded reaction families and combines degenerate reactions. + The generated reactions are deflated. + """ speciesTuple = tuple([spc.copy(deep=True) for spc in speciesTuple]) - _labelListOfSpecies(speciesTuple) - - combos = getMoleculeTuples(speciesTuple) - - reactions = map(reactMolecules,combos) - reactions = list(itertools.chain.from_iterable(reactions)) - # remove reverse reaction - reactions = findDegeneracies(reactions, sameReactants) - # add reverse attribute to families with ownReverse - toDelete = [] - for i, rxn in enumerate(reactions): - family = getDB('kinetics').families[rxn.family] - if family.ownReverse: - successful = family.addReverseAttribute(rxn) - if not successful: - toDelete.append(i) - # delete reactions which we could not find a reverse reaction for - for i in reversed(toDelete): - del reactions[i] - # get a molecule list with species indexes - zippedList = [] - for spec in speciesTuple: - for mol in spec.molecule: - zippedList.append((mol,spec.index)) - - molecules, reactantIndices = zip(*zippedList) + reactions = getDB('kinetics').generate_reactions_from_families(speciesTuple) deflate(reactions, [spec for spec in speciesTuple], @@ -108,222 +79,6 @@ def reactSpecies(speciesTuple): return reactions -def _labelListOfSpecies(speciesTuple): - """ - given a list or tuple of species' objects, ensure all their atoms' id are - independent. - - Modifies the speciesTuple in place, nothing returned. - """ -# assert that all species' atomlabels are different - def independentIDs(): - num_atoms = 0 - IDs = [] - for species in speciesTuple: - num_atoms += len(species.molecule[0].atoms) - IDs.extend([atom.id for atom in species.molecule[0].atoms ]) - num_ID = len(set(IDs)) - return num_ID == num_atoms - # if they are different, relabel and remake atomIDs - if not independentIDs(): - logging.debug('identical atom ids found between species. regenerating') - for species in speciesTuple: - mol = species.molecule[0] - mol.assignAtomIDs() - # remake resonance isomers with new labeles - species.molecule = [mol] - species.generateResonanceIsomers(keepIsomorphic = True) - -def getMoleculeTuples(speciesTuple): - """ - returns a list of molule tuples from given speciesTuples. - - The species objects should already have resonance isomers - generated for the function to work - """ - combos = [] - if len(speciesTuple) == 1:#unimolecular reaction - spc, = speciesTuple - mols = [(mol, spc.index) for mol in spc.molecule] - combos.extend([(combo,) for combo in mols]) - elif len(speciesTuple) == 2:#bimolecular reaction - spcA, spcB = speciesTuple - molsA = [(mol, spcA.index) for mol in spcA.molecule] - molsB = [(mol, spcB.index) for mol in spcB.molecule] - combos.extend(itertools.product(molsA, molsB)) - return combos - -def reactMolecules(moleculeTuples): - """ - Performs a reaction between - the resonance isomers. - - The parameter contains a list of tuples with each tuple: - (Molecule, index of the core species it belongs to) - """ - - families = getDB('kinetics').families - - molecules, reactantIndices = zip(*moleculeTuples) - - reactionList = [] - for _, family in families.iteritems(): - rxns = family.generateReactions(molecules) - reactionList.extend(rxns) - - for reactant in molecules: - reactant.clearLabeledAtoms() - - return reactionList - -def findDegeneracies(rxnList, sameReactants=None): - """ - given a list of Reaction object with Molecule objects, this method - removes degenerate reactions and increments the degeneracy of the - reaction object. For multiple transition states, this method adds - them as separate duplicate reactions. This method modifies - rxnList in place and does not return anything. - - This algorithm used to exist in family.__generateReactions, but was moved - here because it didn't have any family dependence. - """ - - # We want to sort all the reactions into sublists composed of isomorphic reactions - # with degenerate transition states - rxnSorted = [] - for rxn0 in rxnList: - # find resonance structures for rxn0 - convertToSpeciesObjects(rxn0) - if len(rxnSorted) == 0: - # This is the first reaction, so create a new sublist - rxnSorted.append([rxn0]) - else: - # Loop through each sublist, which represents a unique reaction - for rxnList1 in rxnSorted: - # Try to determine if the current rxn0 is identical or isomorphic to any reactions in the sublist - isomorphic = False - identical = False - sameTemplate = False - for rxn in rxnList1: - isomorphic = rxn0.isIsomorphic(rxn, checkIdentical=False, checkTemplateRxnProducts=True) - if not isomorphic: - identical = False - else: - identical = rxn0.isIsomorphic(rxn, checkIdentical=True, checkTemplateRxnProducts=True) - sameTemplate = frozenset(rxn.template) == frozenset(rxn0.template) - if not isomorphic: - # a different product was found, go to next list - break - elif not sameTemplate: - # a different transition state was found, mark as duplicate and - # go to the next sublist - rxn.duplicate = True - rxn0.duplicate = True - break - elif identical: - # An exact copy of rxn0 is already in our list, so we can move on to the next rxn - break - else: # sameTemplate and isomorphic but not identical - # This is the right sublist for rxn0, but continue to see if there is an identical rxn - continue - else: - # We did not break, so this is the right sublist, but there is no identical reaction - # This means that we should add rxn0 to the sublist as a degenerate rxn - rxnList1.append(rxn0) - if isomorphic and sameTemplate: - # We already found the right sublist, so we can move on to the next rxn - break - else: - # We did not break, which means that there was no isomorphic sublist, so create a new one - rxnSorted.append([rxn0]) - - rxnList = [] - for rxnList1 in rxnSorted: - # Collapse our sorted reaction list by taking one reaction from each sublist - rxn = rxnList1[0] - # The degeneracy of each reaction is the number of reactions that were in the sublist - rxn.degeneracy = sum([reaction0.degeneracy for reaction0 in rxnList1]) - rxnList.append(rxn) - - for rxn in rxnList: - if rxn.isForward: - reduceSameReactantDegeneracy(rxn, sameReactants) - else: - # fix the degeneracy of (not ownReverse) reactions found in the backwards direction - correctDegeneracyOfReverseReaction(rxn) - - return rxnList - -def convertToSpeciesObjects(reaction): - """ - modifies a reaction holding Molecule objects to a reaction holding - Species objects, with generated resonance isomers. - """ - # if already species' objects, return none - if isinstance(reaction.reactants[0],Species): - return None - # obtain species with all resonance isomers - for i, mol in enumerate(reaction.reactants): - spec = Species(molecule = [mol]) - if not reaction.isForward: - spec.generateResonanceIsomers(keepIsomorphic=True) - reaction.reactants[i] = spec - for i, mol in enumerate(reaction.products): - spec = Species(molecule = [mol]) - if reaction.isForward: - spec.generateResonanceIsomers(keepIsomorphic=True) - reaction.products[i] = spec - - # convert reaction.pairs object to species - newPairs=[] - for reactant, product in reaction.pairs: - newPair = [] - for reactant0 in reaction.reactants: - if reactant0.isIsomorphic(reactant): - newPair.append(reactant0) - break - for product0 in reaction.products: - if product0.isIsomorphic(product): - newPair.append(product0) - break - newPairs.append(newPair) - reaction.pairs = newPairs - - try: - convertToSpeciesObjects(reaction.reverse) - except AttributeError: - pass - -def reduceSameReactantDegeneracy(reaction, sameReactants=None): - """ - This method reduces the degeneracy of reactions with identical reactants, - since translational component of the transition states are already taken - into account (so swapping the same reactant is not valid) - - This comes from work by Bishop and Laidler in 1965 - """ - if len(reaction.reactants) == 2 and ( - (reaction.isForward and sameReactants) or - reaction.reactants[0].isIsomorphic(reaction.reactants[1]) - ): - reaction.degeneracy *= 0.5 - logging.debug('Degeneracy of reaction {} was decreased by 50% to {} since the reactants are identical'.format(reaction,reaction.degeneracy)) - -def correctDegeneracyOfReverseReaction(reaction): - """ - This method corrects the degeneracy of reactions found when the backwards - template is used. Given the following parameters: - - reaction - list of reactions with their degeneracies already counted - - This method modifies reaction in place and returns nothing - - This does not adjust for identical reactants, you need to use `reduceSameReactantDegeneracy` - to adjust for that. - """ - family = getDB('kinetics').families[reaction.family] - if not family.ownReverse: - reaction.degeneracy = family.calculateDegeneracy(reaction) def deflate(rxns, species, reactantIndices): """ diff --git a/rmgpy/rmg/reactTest.py b/rmgpy/rmg/reactTest.py index fa6d9c6bde..477e9d3ee6 100644 --- a/rmgpy/rmg/reactTest.py +++ b/rmgpy/rmg/reactTest.py @@ -37,9 +37,10 @@ from rmgpy.data.rmg import RMGDatabase from rmgpy.molecule import Molecule from rmgpy.reaction import Reaction +from rmgpy.species import Species from rmgpy.rmg.main import RMG -from rmgpy.rmg.react import * +from rmgpy.rmg.react import react, reactAll, deflate, deflateReaction ################################################### @@ -66,33 +67,6 @@ def setUp(self): reactionLibraries=[] ) - def testReactMolecules(self): - """ - Test that reaction generation for Molecule objects works. - """ - - moleculeTuples = [(Molecule(SMILES='CC'), -1), (Molecule(SMILES='[CH3]'), -1)] - - reactionList = reactMolecules(moleculeTuples) - - self.assertIsNotNone(reactionList) - self.assertTrue(all([isinstance(rxn, TemplateReaction) for rxn in reactionList])) - - def test_labelListofSpecies(self): - """ - Ensure labelListofSpecies modifies atomlabels - """ - from rmgpy.rmg.react import _labelListOfSpecies - s1 = Species().fromSMILES('CCC') - s2 = Species().fromSMILES('C=C[CH]C') - self.assertEqual(s2.molecule[0].atoms[0].id, -1) - - _labelListOfSpecies([s1, s2]) - # checks atom id - self.assertNotEqual(s2.molecule[0].atoms[0].id, -1) - # checks second resonance structure id - self.assertNotEqual(s2.molecule[1].atoms[0].id, -1) - def testReact(self): """ Test that reaction generation from the available families works. diff --git a/rmgpy/rmg/rmgTest.py b/rmgpy/rmg/rmgTest.py index bf71b5ff81..9c54f6ee5f 100644 --- a/rmgpy/rmg/rmgTest.py +++ b/rmgpy/rmg/rmgTest.py @@ -86,7 +86,7 @@ def testDeterministicReactionTemplateMatching(self): # react spc = Species().fromSMILES("O=C[C]=C") - spc.generateResonanceIsomers() + spc.generate_resonance_structures() newReactions = [] newReactions.extend(react((spc,))) @@ -122,7 +122,7 @@ def testCheckForExistingSpeciesForBiAromatics(self): rmg_test = RMG() rmg_test.reactionModel = CoreEdgeReactionModel() DPP = Species().fromSMILES('C1=CC=C(C=C1)CCCC1C=CC=CC=1') - DPP.generateResonanceIsomers() + DPP.generate_resonance_structures() formula = DPP.molecule[0].getFormula() if formula in rmg_test.reactionModel.speciesDict: rmg_test.reactionModel.speciesDict[formula].append(DPP) diff --git a/rmgpy/rmg/settings.py b/rmgpy/rmg/settings.py index e3463eb44b..21d5d9b2c5 100644 --- a/rmgpy/rmg/settings.py +++ b/rmgpy/rmg/settings.py @@ -54,6 +54,7 @@ ================================================================================================================================================== """ import numpy +from rmgpy.quantity import Quantity class ModelSettings(object): """ @@ -63,7 +64,8 @@ def __init__(self,toleranceMoveToCore=None, toleranceMoveEdgeReactionToCore=nump toleranceMoveEdgeReactionToSurface=numpy.inf, toleranceMoveSurfaceSpeciesToCore=numpy.inf, toleranceMoveSurfaceReactionToCore=numpy.inf, toleranceMoveEdgeReactionToSurfaceInterrupt=None,toleranceMoveEdgeReactionToCoreInterrupt=None, maximumEdgeSpecies=1000000, minCoreSizeForPrune=50, minSpeciesExistIterationsForPrune=2, filterReactions=False, ignoreOverallFluxCriterion=False, maxNumSpecies=None, maxNumObjsPerIter=1, - terminateAtMaxObjects=False,toleranceThermoKeepSpeciesInEdge=numpy.inf): + terminateAtMaxObjects=False,toleranceThermoKeepSpeciesInEdge=numpy.inf,dynamicsTimeScale = Quantity((0.0,'sec'))): + self.fluxToleranceKeepInEdge = toleranceKeepInEdge self.fluxToleranceMoveToCore = toleranceMoveToCore @@ -79,7 +81,8 @@ def __init__(self,toleranceMoveToCore=None, toleranceMoveEdgeReactionToCore=nump self.toleranceMoveSurfaceReactionToCore = toleranceMoveSurfaceReactionToCore self.toleranceThermoKeepSpeciesInEdge = toleranceThermoKeepSpeciesInEdge self.terminateAtMaxObjects = terminateAtMaxObjects - + self.dynamicsTimeScale = dynamicsTimeScale.value_si + if toleranceInterruptSimulation: self.fluxToleranceInterrupt = toleranceInterruptSimulation else: diff --git a/rmgpy/solver/base.pyx b/rmgpy/solver/base.pyx index 7fc7dd2ceb..646aef44af 100644 --- a/rmgpy/solver/base.pyx +++ b/rmgpy/solver/base.pyx @@ -534,7 +534,7 @@ cdef class ReactionSystem(DASx): cdef numpy.ndarray[numpy.float64_t,ndim=1] surfaceTotalDivAccumNums, surfaceSpeciesRateRatios cdef numpy.ndarray[numpy.float64_t, ndim=1] forwardRateCoefficients, coreSpeciesConcentrations cdef double prevTime, totalMoles, c, volume, RTP, unimolecularThresholdVal, bimolecularThresholdVal - cdef bool firstTime, useDynamics, terminateAtMaxObjects, schanged + cdef bool useDynamicsTemp, firstTime, useDynamics, terminateAtMaxObjects, schanged cdef numpy.ndarray[numpy.float64_t, ndim=1] edgeReactionRates cdef double reactionRate, production, consumption cdef numpy.ndarray[numpy.int_t,ndim=1] surfaceSpeciesIndices, surfaceReactionIndices @@ -573,8 +573,11 @@ cdef class ReactionSystem(DASx): sensitivityRelativeTolerance = simulatorSettings.sens_rtol filterReactions = modelSettings.filterReactions maxNumObjsPerIter = modelSettings.maxNumObjsPerIter + #if not pruning always terminate at max objects, otherwise only do so if terminateAtMaxObjects=True terminateAtMaxObjects = True if not prune else modelSettings.terminateAtMaxObjects + + dynamicsTimeScale = modelSettings.dynamicsTimeScale useDynamics = not (toleranceMoveEdgeReactionToCore == numpy.inf and toleranceMoveEdgeReactionToSurface == numpy.inf) @@ -589,7 +592,7 @@ cdef class ReactionSystem(DASx): surfaceSpeciesIndices = self.surfaceSpeciesIndices surfaceReactionIndices = self.surfaceReactionIndices - + totalDivAccumNums = None #the product of the ratios between accumulation numbers with and without a given reaction for products and reactants invalidObjects = [] newSurfaceReactions = [] @@ -628,19 +631,51 @@ cdef class ReactionSystem(DASx): stepTime = 1e-12 prevTime = self.t + + firstTime = True + while not terminated: # Integrate forward in time by one time step - try: - self.step(stepTime) - except DASxError as e: - logging.error("Trying to step from time {} to {}".format(prevTime, stepTime)) - logging.error("Core species names: {!r}".format([getSpeciesIdentifier(s) for s in coreSpecies])) - logging.error("Core species moles: {!r}".format(self.y[:numCoreSpecies])) - logging.error("Volume: {!r}".format(self.V)) - logging.error("Core species net rates: {!r}".format(self.coreSpeciesRates)) - logging.error("Edge species net rates: {!r}".format(self.edgeSpeciesRates)) - logging.error("Network leak rates: {!r}".format(self.networkLeakRates)) - raise e + + if not firstTime: + try: + self.step(stepTime) + except DASxError as e: + logging.error("Trying to step from time {} to {} resulted in a solver (DASPK) error".format(prevTime, stepTime)) + + logging.info('Resurrecting Model...') + + if invalidObjects == []: + #species flux criterion + if len(edgeSpeciesRateRatios) > 0: + ind = numpy.argmax(edgeSpeciesRateRatios) + obj = edgeSpecies[ind] + logging.info('At time {0:10.4e} s, species {1} at rate ratio {2} was added to model core in model resurrection process'.format(self.t, obj,maxEdgeSpeciesRates[ind])) + invalidObjects.append(obj) + + if totalDivAccumNums and len(totalDivAccumNums) > 0: #if dynamics data available + ind = numpy.argmax(totalDivAccumNums) + obj = edgeReactions[ind] + logging.info('At time {0:10.4e} s, Reaction {1} at dynamics number {2} was added to model core in model resurrection process'.format(self.t, obj,totalDivAccumNums[ind])) + invalidObjects.append(obj) + + if pdepNetworks != [] and networkLeakRateRatios != []: + ind = numpy.argmax(networkLeakRateRatios) + obj = pdepNetworks[ind] + logging.info('At time {0:10.4e} s, PDepNetwork #{1:d} at network leak rate {2} was sent for exploring during model resurrection process'.format(self.t, obj.index, networkLeakRateRatios[ind])) + invalidObjects.append(obj) + + if invalidObjects != []: + return False,True,invalidObjects,surfaceSpecies,surfaceReactions + else: + logging.error('Model Resurrection has failed') + logging.error("Core species names: {!r}".format([getSpeciesIdentifier(s) for s in coreSpecies])) + logging.error("Core species moles: {!r}".format(self.y[:numCoreSpecies])) + logging.error("Volume: {!r}".format(self.V)) + logging.error("Core species net rates: {!r}".format(self.coreSpeciesRates)) + logging.error("Edge species net rates: {!r}".format(self.edgeSpeciesRates)) + logging.error("Network leak rates: {!r}".format(self.networkLeakRates)) + raise ValueError('invalidObjects could not be filled during resurrection process') y_coreSpecies = self.y[:numCoreSpecies] totalMoles = numpy.sum(y_coreSpecies) @@ -707,15 +742,14 @@ cdef class ReactionSystem(DASx): maxSpecies = edgeSpecies[maxSpeciesIndex] maxSpeciesRate = edgeSpeciesRates[maxSpeciesIndex] logging.info('At time {0:10.4e} s, species {1} was added to model core to avoid singularity'.format(self.t, maxSpecies)) - self.logRates(charRate, maxSpecies, maxSpeciesRate, numpy.inf, maxNetwork, maxNetworkRate) - self.logConversions(speciesIndex, y0) invalidObjects.append(maxSpecies) break - if useDynamics: + if useDynamics and not firstTime and self.t >= dynamicsTimeScale: ####################################################### # Calculation of dynamics criterion for edge reactions# ####################################################### + totalDivAccumNums = numpy.ones(numEdgeReactions) for index in xrange(numEdgeReactions): reactionRate = edgeReactionRates[index] @@ -872,11 +906,9 @@ cdef class ReactionSystem(DASx): tempNewObjects = [] tempNewObjectInds = [] tempNewObjectVals = [] - - if useDynamics: - - #movement of reactions to core/surface based on dynamics number - + + if useDynamics and not firstTime and self.t >= dynamicsTimeScale: + #movement of reactions to core/surface based on dynamics number validLayeringIndices = self.validLayeringIndices tempSurfaceObjects = [] @@ -971,6 +1003,9 @@ cdef class ReactionSystem(DASx): if schanged: #reinitialize surface surfaceSpecies,surfaceReactions = self.initialize_surface(coreSpecies,coreReactions,surfaceSpecies,surfaceReactions) schanged = False + + if firstTime: #turn off firstTime + firstTime = False if interrupt: #breaks while loop terminating iterations logging.info('terminating simulation due to interrupt...') @@ -1034,7 +1069,7 @@ cdef class ReactionSystem(DASx): # Return the invalid object (if the simulation was invalid) or None # (if the simulation was valid) - return terminated, invalidObjects, surfaceSpecies, surfaceReactions + return terminated, False, invalidObjects, surfaceSpecies, surfaceReactions cpdef logRates(self, double charRate, object species, double speciesRate, double maxDifLnAccumNum, object network, double networkRate): """ diff --git a/rmgpy/solver/baseTest.py b/rmgpy/solver/baseTest.py index 4a58bb31dd..98c946c510 100644 --- a/rmgpy/solver/baseTest.py +++ b/rmgpy/solver/baseTest.py @@ -164,7 +164,7 @@ def testListen(self): simulatorSettings = SimulatorSettings() # run simulation: - terminated, obj,sspcs,srxns = reactionSystem.simulate( + terminated,resurrected,obj,sspcs,srxns = reactionSystem.simulate( coreSpecies = reactionModel.core.species, coreReactions = reactionModel.core.reactions, edgeSpecies = reactionModel.edge.species, diff --git a/rmgpy/solver/liquidTest.py b/rmgpy/solver/liquidTest.py index c4e6f3b08c..de25fded6f 100644 --- a/rmgpy/solver/liquidTest.py +++ b/rmgpy/solver/liquidTest.py @@ -460,9 +460,3 @@ def tearDown(self): import rmgpy.data.rmg rmgpy.data.rmg.database = None - - from rmgpy.rmg.model import Species as DifferentSpecies - DifferentSpecies.solventData = None - DifferentSpecies.solventName = None - DifferentSpecies.solventStructure = None - DifferentSpecies.solventViscosity = None diff --git a/rmgpy/species.pxd b/rmgpy/species.pxd index 3e442bb667..7f15d30942 100644 --- a/rmgpy/species.pxd +++ b/rmgpy/species.pxd @@ -49,8 +49,10 @@ cdef class Species: cdef public dict props cdef public str aug_inchi cdef public float symmetryNumber - - cpdef generateResonanceIsomers(self,bint keepIsomorphic=?) + cdef public bint isSolvent + cdef public int creationIteration + + cpdef generate_resonance_structures(self,bint keepIsomorphic=?) cpdef bint isIsomorphic(self, other) except -2 diff --git a/rmgpy/species.py b/rmgpy/species.py index 84e7c977ea..040ed440b1 100644 --- a/rmgpy/species.py +++ b/rmgpy/species.py @@ -46,6 +46,7 @@ import numpy import cython import logging +from operator import itemgetter import rmgpy.quantity as quantity @@ -79,15 +80,20 @@ class Species(object): always considered regardless of this variable `props` A generic 'properties' dictionary to store user-defined flags `aug_inchi` Unique augmented inchi + `isSolvent` Boolean describing whether this species is the solvent + `creationIteration` Iteration which the species is created within the reaction mechanism generation algorithm ======================= ==================================================== note: :class:`rmg.model.Species` inherits from this class, and adds some extra methods. """ + # these are class level attributes? + + def __init__(self, index=-1, label='', thermo=None, conformer=None, molecule=None, transportData=None, molecularWeight=None, energyTransferModel=None, reactive=True, props=None, aug_inchi=None, - symmetryNumber = -1): + symmetryNumber = -1, creationIteration = 0): self.index = index self.label = label self.thermo = thermo @@ -100,13 +106,14 @@ def __init__(self, index=-1, label='', thermo=None, conformer=None, self.props = props or {} self.aug_inchi = aug_inchi self.symmetryNumber = symmetryNumber + self.isSolvent = False + self.creationIteration = creationIteration # Check multiplicity of each molecule is the same if molecule is not None and len(molecule)>1: mult = molecule[0].multiplicity for m in molecule[1:]: if mult != m.multiplicity: raise SpeciesError('Multiplicities of molecules in species {species} do not match.'.format(species=label)) - @@ -155,17 +162,17 @@ def setMolecularWeight(self, value): self._molecularWeight = quantity.Mass(value) molecularWeight = property(getMolecularWeight, setMolecularWeight, """The molecular weight of the species. (Note: value_si is in kg/molecule not kg/mole)""") - def generateResonanceIsomers(self, keepIsomorphic=True): + def generate_resonance_structures(self, keepIsomorphic=True): """ - Generate all of the resonance isomers of this species. The isomers are + Generate all of the resonance structures of this species. The isomers are stored as a list in the `molecule` attribute. If the length of `molecule` is already greater than one, it is assumed that all of the - resonance isomers have already been generated. + resonance structures have already been generated. """ if len(self.molecule) == 1: if not self.molecule[0].atomIDValid(): self.molecule[0].assignAtomIDs() - self.molecule = self.molecule[0].generateResonanceIsomers(keepIsomorphic) + self.molecule = self.molecule[0].generate_resonance_structures(keepIsomorphic) def isIsomorphic(self, other): """ @@ -415,7 +422,7 @@ def getResonanceHybrid(self): of all the resonance structures. """ # get labeled resonance isomers - self.generateResonanceIsomers(keepIsomorphic=True) + self.generate_resonance_structures(keepIsomorphic=True) # return if no resonance if len(self.molecule) == 1: @@ -519,21 +526,25 @@ def copy(self, deep=False): def getAugmentedInChI(self): if self.aug_inchi is None: self.aug_inchi = self.generate_aug_inchi() - return self.aug_inchi - else: - return self.aug_inchi + return self.aug_inchi def generate_aug_inchi(self): candidates = [] - self.generateResonanceIsomers() + self.generate_resonance_structures() for mol in self.molecule: - cand = mol.toAugmentedInChI() - candidates.append(cand) - - candidates.sort() - return candidates[0] + try: + cand = [mol.toAugmentedInChI(),mol] + except ValueError: + pass # not all resonance structures can be parsed into InChI (e.g. if containing a hypervalance atom) + else: + candidates.append(cand) + candidates = sorted(candidates, key=itemgetter(0)) + for cand in candidates: + if all(atom.charge == 0 for atom in cand[1].vertices): + return cand[0] + return candidates[0][0] - def getThermoData(self): + def getThermoData(self, solventName = ''): """ Returns a `thermoData` object of the current Species object. @@ -553,7 +564,7 @@ def getThermoData(self): if not isinstance(self.thermo, (NASA, Wilhoit, ThermoData)): self.thermo = self.thermo.result() else: - submit(self) + submit(self, solventName) if not isinstance(self.thermo, (NASA, Wilhoit, ThermoData)): self.thermo = self.thermo.result() diff --git a/rmgpy/speciesTest.py b/rmgpy/speciesTest.py index bb51791bb2..7604e2a637 100644 --- a/rmgpy/speciesTest.py +++ b/rmgpy/speciesTest.py @@ -163,14 +163,14 @@ def testSpeciesProps_object_attribute(self): def testResonanceIsomersGenerated(self): "Test that 1-penten-3-yl makes 2-penten-1-yl resonance isomer" spec = Species().fromSMILES('C=C[CH]CC') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() self.assertEquals(len(spec.molecule), 2) self.assertEquals(spec.molecule[1].toSMILES(), "[CH2]C=CCC") def testResonaceIsomersRepresented(self): "Test that both resonance forms of 1-penten-3-yl are printed by __repr__" spec = Species().fromSMILES('C=C[CH]CC') - spec.generateResonanceIsomers() + spec.generate_resonance_structures() exec('spec2 = {0!r}'.format(spec)) self.assertEqual(len(spec.molecule), len(spec2.molecule)) for i, j in zip(spec.molecule, spec2.molecule): diff --git a/rmgpy/thermo/nasaTest.py b/rmgpy/thermo/nasaTest.py index 394fa7f140..789af7258e 100644 --- a/rmgpy/thermo/nasaTest.py +++ b/rmgpy/thermo/nasaTest.py @@ -74,12 +74,6 @@ def tearDown(self): import rmgpy.data.rmg rmgpy.data.rmg.database = None - from rmgpy.rmg.model import Species as DifferentSpecies - DifferentSpecies.solventData = None - DifferentSpecies.solventName = None - DifferentSpecies.solventStructure = None - DifferentSpecies.solventViscosity = None - def test_polyLow(self): """ Test that the NASA low-temperature polynomial was properly set. diff --git a/rmgpy/thermo/thermodata.pyx b/rmgpy/thermo/thermodata.pyx index 9cd132981a..17a6178b2a 100644 --- a/rmgpy/thermo/thermodata.pyx +++ b/rmgpy/thermo/thermodata.pyx @@ -28,6 +28,7 @@ ################################################################################ import numpy import cython +import logging from libc.math cimport sqrt, log @@ -303,6 +304,11 @@ cdef class ThermoData(HeatCapacityModel): slope = (Cphigh - Cplow) / (Thigh - Tlow) intercept = (Cplow * Thigh - Cphigh * Tlow) / (Thigh - Tlow) if slope > 0: + if CpInf < Cphigh: + logging.warning("Cphigh is above the theoretical CpInf value for ThermoData object\n{0}." + "\nThe thermo for this species is probably wrong! Setting CpInf = Cphigh for Entropy calculation" + "at T = {1} K...".format(self,T)) + CpInf = Cphigh T0 = (CpInf - Cphigh) / slope + Thigh if T <= T0: S += slope * (T - Thigh) + intercept * log(T / Thigh) diff --git a/rmgpy/thermo/thermoengine.py b/rmgpy/thermo/thermoengine.py index b32ffb2ea7..1d5b0b40b1 100644 --- a/rmgpy/thermo/thermoengine.py +++ b/rmgpy/thermo/thermoengine.py @@ -37,7 +37,7 @@ from rmgpy.thermo import Wilhoit, NASA, ThermoData import rmgpy.data.rmg -def processThermoData(spc, thermo0, thermoClass=NASA): +def processThermoData(spc, thermo0, thermoClass=NASA, solventName = ''): """ Converts via Wilhoit into required `thermoClass` and sets `E0`. @@ -57,11 +57,17 @@ def processThermoData(spc, thermo0, thermoClass=NASA): wilhoit = thermo0.toWilhoit() # Add on solvation correction - if Species.solventData and not "Liquid thermo library" in thermo0.comment: + solvationdatabase = getDB('solvation') + if not solventName or solvationdatabase is None: + logging.debug('Solvent database or solventName not found. Solvent effect was not utilized') + solventData = None + else: + solventData = solvationdatabase.getSolventData(solventName) + if solventData and not "Liquid thermo library" in thermo0.comment: solvationdatabase = getDB('solvation') #logging.info("Making solvent correction for {0}".format(Species.solventName)) soluteData = solvationdatabase.getSoluteData(spc) - solvation_correction = solvationdatabase.getSolvationCorrection(soluteData, Species.solventData) + solvation_correction = solvationdatabase.getSolvationCorrection(soluteData, solventData) # correction is added to the entropy and enthalpy wilhoit.S0.value_si = (wilhoit.S0.value_si + solvation_correction.entropy) wilhoit.H0.value_si = (wilhoit.H0.value_si + solvation_correction.enthalpy) @@ -75,7 +81,7 @@ def processThermoData(spc, thermo0, thermoClass=NASA): if thermoClass is Wilhoit: thermo = wilhoit elif thermoClass is NASA: - if Species.solventData: + if solventData: #if liquid phase simulation keep the nasa polynomial if it comes from a liquid phase thermoLibrary. Otherwise convert wilhoit to NASA if "Liquid thermo library" in thermo0.comment and isinstance(thermo0, NASA): thermo = thermo0 @@ -106,7 +112,7 @@ def processThermoData(spc, thermo0, thermoClass=NASA): return thermo -def generateThermoData(spc, thermoClass=NASA): +def generateThermoData(spc, thermoClass=NASA, solventName=''): """ Generates thermo data, first checking Libraries, then using either QM or Database. @@ -145,10 +151,10 @@ def generateThermoData(spc, thermoClass=NASA): thermoCentralDatabase.registerInCentralThermoDB(spc) - return processThermoData(spc, thermo0, thermoClass) + return processThermoData(spc, thermo0, thermoClass, solventName) -def evaluator(spc): +def evaluator(spc, solventName = ''): """ Module-level function passed to workers. @@ -161,12 +167,12 @@ def evaluator(spc): """ logging.debug("Evaluating spc %s ", spc) - spc.generateResonanceIsomers() - thermo = generateThermoData(spc) + spc.generate_resonance_structures() + thermo = generateThermoData(spc,solventName=solventName) return thermo -def submit(spc): +def submit(spc, solventName = ''): """ Submits a request to calculate chemical data for the Species object. @@ -176,4 +182,4 @@ def submit(spc): the result. """ - spc.thermo = submit_(evaluator, spc) + spc.thermo = submit_(evaluator, spc, solventName= solventName) diff --git a/rmgpy/thermo/thermoengineTest.py b/rmgpy/thermo/thermoengineTest.py index fec6356999..8c99a0f5bd 100644 --- a/rmgpy/thermo/thermoengineTest.py +++ b/rmgpy/thermo/thermoengineTest.py @@ -67,12 +67,6 @@ def tearDown(): """ import rmgpy.data.rmg rmgpy.data.rmg.database = None - - from rmgpy.rmg.model import Species as DifferentSpecies - DifferentSpecies.solventData = None - DifferentSpecies.solventName = None - DifferentSpecies.solventStructure = None - DifferentSpecies.solventViscosity = None def funcSubmit(): """ diff --git a/rmgpy/thermo/wilhoitTest.py b/rmgpy/thermo/wilhoitTest.py index c97f265abd..0a92b05072 100644 --- a/rmgpy/thermo/wilhoitTest.py +++ b/rmgpy/thermo/wilhoitTest.py @@ -84,12 +84,6 @@ def tearDown(self): """ import rmgpy.data.rmg rmgpy.data.rmg.database = None - - from rmgpy.rmg.model import Species as DifferentSpecies - DifferentSpecies.solventData = None - DifferentSpecies.solventName = None - DifferentSpecies.solventStructure = None - DifferentSpecies.solventViscosity = None def test_Cp0(self): """ diff --git a/rmgpy/tools/canteraModel.py b/rmgpy/tools/canteraModel.py index d31b92c1cd..349da63aec 100644 --- a/rmgpy/tools/canteraModel.py +++ b/rmgpy/tools/canteraModel.py @@ -515,7 +515,7 @@ def getRMGSpeciesFromUserSpecies(userList, RMGList): """ mapping = {} for userSpecies in userList: - userSpecies.generateResonanceIsomers() + userSpecies.generate_resonance_structures() for rmgSpecies in RMGList: if userSpecies.isIsomorphic(rmgSpecies): diff --git a/rmgpy/tools/data/flux/input_liquid.py b/rmgpy/tools/data/flux/input_liquid.py new file mode 100644 index 0000000000..b22f7593fc --- /dev/null +++ b/rmgpy/tools/data/flux/input_liquid.py @@ -0,0 +1,54 @@ +# Data sources +database( + thermoLibraries = ['primaryThermoLibrary'], + reactionLibraries = [], + seedMechanisms = [], + kineticsFamilies = 'default', + kineticsEstimator = 'rate rules', +) + +# List of species +species( + label='ethane', + reactive=True, + structure=SMILES("CC"), +) + +species( + label='heptane', + reactive=True, + structure=SMILES("CCCCCCC"), +) + +# Reaction systems +liquidReactor( + temperature=(700,'K'), + initialConcentrations={ + "ethane": (1.0e-3, 'mol/cm^3'), + }, + terminationTime=(1e6,'s'), + constantSpecies=['ethane'] +) + +solvation( + solvent='heptane' +) + +simulator( + atol=1e-16, + rtol=1e-8, +) + +model( + toleranceKeepInEdge=0.0, + toleranceMoveToCore=0.1, + toleranceInterruptSimulation=0.1, + maximumEdgeSpecies=100000 +) + +options( + units='si', + saveRestartPeriod=None, + generatePlots=False, + saveSimulationProfiles=True, +) diff --git a/rmgpy/tools/data/flux/input.py b/rmgpy/tools/data/flux/input_simple.py similarity index 100% rename from rmgpy/tools/data/flux/input.py rename to rmgpy/tools/data/flux/input_simple.py diff --git a/rmgpy/tools/fluxdiagram.py b/rmgpy/tools/fluxdiagram.py index be331b06f0..4cef34dce9 100644 --- a/rmgpy/tools/fluxdiagram.py +++ b/rmgpy/tools/fluxdiagram.py @@ -41,8 +41,8 @@ import pydot from rmgpy.solver.base import TerminationTime, TerminationConversion -from rmgpy.solver.simple import SimpleReactor -import rmgpy.util as util +from rmgpy.solver.liquid import LiquidReactor +from rmgpy.kinetics.diffusionLimited import diffusionLimiter from rmgpy.rmg.settings import SimulatorSettings from .loader import loadRMGJob @@ -81,14 +81,15 @@ def generateFluxDiagram(reactionModel, times, concentrations, reactionRates, out a movie. The individual frames and the final movie are saved on disk at `outputDirectory.` """ - global maximumNodeCount, maximumEdgeCount, timeStep, concentrationTolerance, speciesRateTolerance + global maximumNodeCount, maximumEdgeCount, concentrationTolerance, speciesRateTolerance, maximumNodePenWidth, maximumEdgePenWidth # Allow user defined settings for flux diagram generation if given if settings: - maximumNodeCount = settings['maximumNodeCount'] - maximumEdgeCount = settings['maximumEdgeCount'] - timeStep = settings['timeStep'] - concentrationTolerance = settings['concentrationTolerance'] - speciesRateTolerance = settings['speciesRateTolerance'] + maximumNodeCount = settings.get('maximumNodeCount', maximumNodeCount) + maximumEdgeCount = settings.get('maximumEdgeCount', maximumEdgeCount) + concentrationTolerance = settings.get('concentrationTolerance', concentrationTolerance) + speciesRateTolerance = settings.get('speciesRateTolerance', speciesRateTolerance) + maximumNodePenWidth = settings.get('maximumNodePenWidth', maximumNodePenWidth) + maximumEdgePenWidth= settings.get('maximumEdgePenWidth', maximumEdgePenWidth) # Get the species and reactions corresponding to the provided concentrations and reaction rates speciesList = reactionModel.core.species[:] @@ -284,39 +285,35 @@ def generateFluxDiagram(reactionModel, times, concentrations, reactionRates, out ################################################################################ -def simulate(reactionModel, reactionSystem, settings = None): +def simulate(reactionModel, reactionSystem, settings=None): """ Generate and return a set of core and edge species and reaction fluxes by simulating the given `reactionSystem` using the given `reactionModel`. """ - global maximumNodeCount, maximumEdgeCount, timeStep, concentrationTolerance, speciesRateTolerance + global timeStep # Allow user defined settings for flux diagram generation if given if settings: - maximumNodeCount = settings['maximumNodeCount'] - maximumEdgeCount = settings['maximumEdgeCount'] - timeStep = settings['timeStep'] - concentrationTolerance = settings['concentrationTolerance'] - speciesRateTolerance = settings['speciesRateTolerance'] + timeStep = settings.get('timeStep', timeStep) coreSpecies = reactionModel.core.species coreReactions = reactionModel.core.reactions edgeSpecies = reactionModel.edge.species edgeReactions = reactionModel.edge.reactions -# numCoreSpecies = len(coreSpecies) -# numCoreReactions = len(coreReactions) -# numEdgeSpecies = len(edgeSpecies) -# numEdgeReactions = len(edgeReactions) - speciesIndex = {} for index, spec in enumerate(coreSpecies): speciesIndex[spec] = index simulatorSettings = SimulatorSettings(atol=absoluteTolerance,rtol=relativeTolerance) - reactionSystem.initializeModel(coreSpecies, coreReactions, edgeSpecies, edgeReactions, [], [], [], - atol=simulatorSettings.atol,rtol=simulatorSettings.rtol, - sens_atol=simulatorSettings.sens_atol,sens_rtol=simulatorSettings.sens_rtol) + # Enable constant species for LiquidReactor + if isinstance(reactionSystem, LiquidReactor): + if reactionSystem.constSPCNames is not None: + reactionSystem.get_constSPCIndices(coreSpecies) + + reactionSystem.initializeModel(coreSpecies, coreReactions, edgeSpecies, edgeReactions, + atol=simulatorSettings.atol, rtol=simulatorSettings.rtol, + sens_atol=simulatorSettings.sens_atol, sens_rtol=simulatorSettings.sens_rtol) # Copy the initial conditions to use in evaluating conversions y0 = reactionSystem.y.copy() @@ -327,12 +324,10 @@ def simulate(reactionModel, reactionSystem, settings = None): edgeReactionRates = [] nextTime = initialTime - terminated = False; iteration = 0 + terminated = False while not terminated: # Integrate forward in time to the next time point reactionSystem.advance(nextTime) - - iteration += 1 time.append(reactionSystem.t) coreSpeciesConcentrations.append(reactionSystem.coreSpeciesConcentrations) @@ -446,20 +441,29 @@ def loadChemkinOutput(outputFile, reactionModel): ################################################################################ -def createFluxDiagram(savePath, inputFile, chemkinFile, speciesDict, java = False, settings = None, chemkinOutput = '', centralSpecies = None): +def createFluxDiagram(inputFile, chemkinFile, speciesDict, savePath=None, speciesPath=None, java=False, settings=None, + chemkinOutput='', centralSpecies=None, diffusionLimited=True): """ Generates the flux diagram based on a condition 'inputFile', chemkin.inp chemkinFile, a speciesDict txt file, plus an optional chemkinOutput file. """ - rmg = loadRMGJob(inputFile, chemkinFile, speciesDict, generateImages=True, useJava=java) + if speciesPath is None: + speciesPath = os.path.join(os.path.dirname(inputFile), 'species') + generateImages = True + else: + generateImages = False + + rmg = loadRMGJob(inputFile, chemkinFile, speciesDict, generateImages=generateImages, useJava=java) - speciesPath = os.path.join(os.path.dirname(inputFile), 'species') + if savePath is None: + savePath = os.path.join(rmg.outputDirectory, 'flux') # if you have a chemkin output, then you only have one reactionSystem if chemkinOutput: + outDir = os.path.join(savePath, '1') try: - os.makedirs(os.path.join(savePath,'1')) + os.makedirs(outDir) except OSError: pass @@ -467,18 +471,23 @@ def createFluxDiagram(savePath, inputFile, chemkinFile, speciesDict, java = Fals time, coreSpeciesConcentrations, coreReactionRates, edgeReactionRates = loadChemkinOutput(chemkinOutput, rmg.reactionModel) print 'Generating flux diagram for chemkin output...' - generateFluxDiagram(rmg.reactionModel, time, coreSpeciesConcentrations, coreReactionRates, os.path.join(savePath, '1'), centralSpecies, speciesPath, settings) + generateFluxDiagram(rmg.reactionModel, time, coreSpeciesConcentrations, coreReactionRates, outDir, centralSpecies, speciesPath, settings) else: # Generate a flux diagram video for each reaction system for index, reactionSystem in enumerate(rmg.reactionSystems): + outDir = os.path.join(savePath, '{0:d}'.format(index+1)) try: - os.makedirs(os.path.join(savePath,'{0:d}'.format(index+1))) + os.makedirs(outDir) except OSError: # Fail silently on any OS errors pass - #util.makeOutputSubdirectory('flux/{0:d}'.format(index+1)) + # Enable diffusion-limited rates + if diffusionLimited and isinstance(reactionSystem, LiquidReactor): + rmg.loadDatabase() + solventData = rmg.database.solvation.getSolventData(rmg.solvent) + diffusionLimiter.enable(solventData, rmg.database.solvation) # If there is no termination time, then add one to prevent jobs from # running forever @@ -489,35 +498,5 @@ def createFluxDiagram(savePath, inputFile, chemkinFile, speciesDict, java = Fals time, coreSpeciesConcentrations, coreReactionRates, edgeReactionRates = simulate(rmg.reactionModel, reactionSystem, settings) print 'Generating flux diagram for reaction system {0:d}...'.format(index+1) - generateFluxDiagram(rmg.reactionModel, time, coreSpeciesConcentrations, coreReactionRates, os.path.join(savePath, '{0:d}'.format(index+1)), + generateFluxDiagram(rmg.reactionModel, time, coreSpeciesConcentrations, coreReactionRates, outDir, centralSpecies, speciesPath, settings) - -def run(inputFile, speciesPath=None, useJava=False): - - rmg = loadRMGJob(inputFile, useJava=useJava) - - if speciesPath is None: - speciesPath = os.path.join(os.path.dirname(inputFile), 'species') - - # Generate a flux diagram video for each reaction system - util.makeOutputSubdirectory(rmg.outputDirectory, 'flux') - for index, reactionSystem in enumerate(rmg.reactionSystems): - - util.makeOutputSubdirectory(rmg.outputDirectory, 'flux/{0:d}'.format(index+1)) - - # If there is no termination time, then add one to prevent jobs from - # running forever - if not any([isinstance(term, TerminationTime) for term in reactionSystem.termination]): - reactionSystem.termination.append(TerminationTime((1e10,'s'))) - - - print 'Conducting simulation of reaction system {0:d}...'.format(index+1) - time, coreSpeciesConcentrations, coreReactionRates, edgeReactionRates =\ - simulate(rmg.reactionModel, reactionSystem) - - centralSpecies = None - print 'Generating flux diagram for reaction system {0:d}...'.format(index+1) - generateFluxDiagram( - rmg.reactionModel, time, coreSpeciesConcentrations, coreReactionRates,\ - os.path.join(rmg.outputDirectory, 'flux', '{0:d}'.format(index+1)), centralSpecies, speciesPath - ) diff --git a/rmgpy/tools/fluxtest.py b/rmgpy/tools/fluxtest.py index 939868288b..e32d25afd8 100644 --- a/rmgpy/tools/fluxtest.py +++ b/rmgpy/tools/fluxtest.py @@ -31,15 +31,24 @@ import shutil from nose.plugins.attrib import attr import rmgpy -from rmgpy.tools.fluxdiagram import * +from rmgpy.tools.fluxdiagram import createFluxDiagram @attr('functional') class FluxDiagramTest(unittest.TestCase): - def test_avi(self): + def test_avi_simple(self): folder = os.path.join(os.path.dirname(rmgpy.__file__),'tools','data','flux') - inputFile = os.path.join(folder,'input.py') - run(inputFile) + inputFile = os.path.join(folder,'input_simple.py') + chemkinFile = os.path.join(folder, 'chemkin', 'chem.inp') + dictFile = os.path.join(folder, 'chemkin', 'species_dictionary.txt') + settings = {'maximumNodeCount': 50, + 'maximumEdgeCount': 50, + 'concentrationTolerance': 1e-6, + 'speciesRateTolerance': 1e-6, + 'maximumNodePenWidth': 10.0, + 'maximumEdgePenWidth': 10.0, + 'timeStep': 10**0.1} + createFluxDiagram(inputFile, chemkinFile, dictFile, centralSpecies='ethane', settings=settings) outputdir = os.path.join(folder,'flux') simfile = os.path.join(outputdir,'1','flux_diagram.avi') @@ -51,6 +60,24 @@ def test_avi(self): shutil.rmtree(outputdir) shutil.rmtree(speciesdir) + def test_avi_liquid(self): + folder = os.path.join(os.path.dirname(rmgpy.__file__), 'tools', 'data', 'flux') + + inputFile = os.path.join(folder, 'input_liquid.py') + chemkinFile = os.path.join(folder, 'chemkin', 'chem.inp') + dictFile = os.path.join(folder, 'chemkin', 'species_dictionary.txt') + createFluxDiagram(inputFile, chemkinFile, dictFile, diffusionLimited=False) + + outputdir = os.path.join(folder, 'flux') + simfile = os.path.join(outputdir, '1', 'flux_diagram.avi') + + speciesdir = os.path.join(folder, 'species') + + self.assertTrue(os.path.isfile(simfile)) + + shutil.rmtree(outputdir) + shutil.rmtree(speciesdir) + def tearDown(self): import rmgpy.data.rmg rmgpy.data.rmg.database = None diff --git a/rmgpy/tools/sensitivity.py b/rmgpy/tools/sensitivity.py index 1b9a054660..4d43e80133 100644 --- a/rmgpy/tools/sensitivity.py +++ b/rmgpy/tools/sensitivity.py @@ -38,7 +38,6 @@ from rmgpy.tools.plot import ReactionSensitivityPlot, ThermoSensitivityPlot from rmgpy.rmg.settings import ModelSettings from rmgpy.solver.liquid import LiquidReactor -from rmgpy.rmg.model import Species from rmgpy.kinetics.diffusionLimited import diffusionLimiter def plotSensitivity(outputDirectory, reactionSystemIndex, sensitiveSpeciesList, number=10, fileformat='.png'): @@ -116,10 +115,8 @@ def simulate(rmg, diffusionLimited=True): if isinstance(reactionSystem, LiquidReactor): if diffusionLimited: rmg.loadDatabase() - Species.solventData = rmg.database.solvation.getSolventData(rmg.solvent) - Species.solventName = rmg.solvent - Species.solventStructure = rmg.database.solvation.getSolventStructure(rmg.solvent) - diffusionLimiter.enable(Species.solventData, rmg.database.solvation) + solventData = rmg.database.solvation.getSolventData(rmg.solvent) + diffusionLimiter.enable(solventData, rmg.database.solvation) # Store constant species indices if reactionSystem.constSPCNames is not None: diff --git a/rmgpy/tools/uncertainty.py b/rmgpy/tools/uncertainty.py index 224abf7b3e..397558a913 100644 --- a/rmgpy/tools/uncertainty.py +++ b/rmgpy/tools/uncertainty.py @@ -585,7 +585,7 @@ def sensitivityAnalysis(self, initialMoleFractions, sensitiveSpecies, T, P, term from rmgpy.quantity import Quantity from rmgpy.tools.sensitivity import plotSensitivity from rmgpy.rmg.listener import SimulationProfileWriter, SimulationProfilePlotter - + from rmgpy.rmg.settings import ModelSettings, SimulatorSettings T = Quantity(T) P = Quantity(P) termination=[TerminationTime(Quantity(terminationTime))] @@ -605,14 +605,22 @@ def sensitivityAnalysis(self, initialMoleFractions, sensitiveSpecies, T, P, term reactionSystem.attach(SimulationProfilePlotter( self.outputDirectory, reactionSystemIndex, self.speciesList)) + simulatorSettings = SimulatorSettings() #defaults + + modelSettings = ModelSettings() #defaults + modelSettings.fluxToleranceMoveToCore = 0.1 + modelSettings.fluxToleranceInterrupt = 1.0 + modelSettings.fluxToleranceKeepInEdge = 0.0 + reactionSystem.simulate( coreSpecies = self.speciesList, coreReactions = self.reactionList, edgeSpecies = [], edgeReactions = [], - toleranceKeepInEdge = 0, - toleranceMoveToCore = 1, - toleranceInterruptSimulation = 1, + surfaceSpecies = [], + surfaceReactions = [], + modelSettings = modelSettings, + simulatorSettings = simulatorSettings, sensitivity = True, sensWorksheet = sensWorksheet, ) diff --git a/rmgpy/version.py b/rmgpy/version.py index 582640210f..fa6ad30dcd 100644 --- a/rmgpy/version.py +++ b/rmgpy/version.py @@ -1,3 +1,3 @@ # This file describes the version of RMG-Py -__version__ = '2.1.5' \ No newline at end of file +__version__ = '2.1.6' \ No newline at end of file diff --git a/scripts/generateFluxDiagram.py b/scripts/generateFluxDiagram.py index f4e1ef1d48..26e12a214b 100644 --- a/scripts/generateFluxDiagram.py +++ b/scripts/generateFluxDiagram.py @@ -3,42 +3,59 @@ """ This script generates a video showing the flux diagram for a given reaction -model as it evolves in time. It takes as its lone required argument the path -to an RMG-Py input file corresponding to a job that has already been run. -This script will automatically read from the necessary output files to extract -the information needed to generate the flux diagram. +model as it evolves in time. It takes as its arguments the path to an RMG-Py +input file corresponding to a job that has already been run and the +corresponding Chemkin mechanism and RMG species dictionary files. If a folder +of species images is available, it can be passed as an optional argument. A +Chemkin output file can also be passed as an optional positional argument. """ -import os.path +import os import argparse -import rmgpy.tools.fluxdiagram as fluxdiagram +from rmgpy.tools.fluxdiagram import createFluxDiagram ################################################################################ def parse_arguments(): parser = argparse.ArgumentParser() - parser.add_argument('input', metavar='INPUT', type=str, nargs=1, - help='the RMG input file to use') - parser.add_argument('species', metavar='SPECIES', type=str, nargs='?', default=None, - help='path to species images') + parser.add_argument('input', metavar='INPUT', type=str, help='RMG input file') + parser.add_argument('chemkin', metavar='CHEMKIN', type=str, help='Chemkin file') + parser.add_argument('dictionary', metavar='DICTIONARY', type=str, help='RMG dictionary file') + parser.add_argument('species', metavar='SPECIES', type=str, nargs='?', default=None, help='Path to species images') + parser.add_argument('chemkinOutput', metavar='CHEMKIN_OUTPUT', type=str, nargs='?', default=None, + help='Chemkin output file') parser.add_argument('--java', action='store_true', help='process RMG-Java model') + parser.add_argument('--no-dlim', dest='dlim', action='store_false', help='Turn off diffusion-limited rates') + parser.add_argument('-n', '--maxnode', metavar='N', type=int, help='Maximum number of nodes to show in diagram') + parser.add_argument('-e', '--maxedge', metavar='N', type=int, help='Maximum number of edges to show in diagram') + parser.add_argument('-c', '--conctol', metavar='TOL', type=float, help='Lowest fractional concentration to show') + parser.add_argument('-r', '--ratetol', metavar='TOL', type=float, help='Lowest fractional species rate to show') + parser.add_argument('-t', '--tstep', metavar='S', type=float, + help='Multiplicative factor to use between consecutive time points') args = parser.parse_args() - inputFile = os.path.abspath(args.input[0]) - speciesPath = os.path.abspath(args.species[0]) if args.species is not None else None + inputFile = os.path.abspath(args.input) + chemkinFile = os.path.abspath(args.chemkin) + dictFile = os.path.abspath(args.dictionary) + speciesPath = os.path.abspath(args.species) if args.species is not None else None + chemkinOutput = os.path.abspath(args.chemkinOutput) if args.chemkinOutput is not None else '' useJava = args.java + dflag = args.dlim + + keys = ('maximumNodeCount', 'maximumEdgeCount', 'concentrationTolerance', 'speciesRateTolerance', 'timeStep') + vals = (args.maxnode, args.maxedge, args.conctol, args.ratetol, args.tstep) + settings = {k: v for k, v in zip(keys, vals) if v is not None} - return inputFile, speciesPath, useJava + return inputFile, chemkinFile, dictFile, speciesPath, chemkinOutput, useJava, dflag, settings def main(): - # This might not work anymore because functions were modified for use with webserver - - inputFile, speciesPath, useJava = parse_arguments() + inputFile, chemkinFile, dictFile, speciesPath, chemkinOutput, useJava, dflag, settings = parse_arguments() - fluxdiagram.run(inputFile, speciesPath, useJava) + createFluxDiagram(inputFile, chemkinFile, dictFile, speciesPath=speciesPath, java=useJava, settings=settings, + chemkinOutput=chemkinOutput, diffusionLimited=dflag) if __name__ == '__main__': main() \ No newline at end of file