Skip to content

Commit e88187c

Browse files
committed
refactor thermo extract_source_from_comments
This refactors the extract_source_from_comments function and changes library sources so that a tuple of the library name and the library entry object is returned instead of just the library name
1 parent b5c4844 commit e88187c

2 files changed

Lines changed: 211 additions & 159 deletions

File tree

rmgpy/data/thermo.py

Lines changed: 175 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2854,82 +2854,9 @@ def get_ring_groups_from_comments(self, thermo_data):
28542854

28552855
return ring_groups, polycyclic_groups
28562856

2857-
def extract_source_from_comments(self, species):
2858-
"""
2859-
`species`: A species object containing thermo data and thermo data comments
2860-
2861-
Parses the verbose string of comments from the thermo data of the species object,
2862-
and extracts the thermo sources.
2863-
2864-
Returns a dictionary with keys of 'Library', 'QM', 'ADS', and/or 'GAV'.
2865-
Commonly, species thermo are estimated using only one of these sources.
2866-
However, a radical can be estimated with more than one type of source, for
2867-
instance a saturated library value and a GAV HBI correction, or a QM saturated value
2868-
and a GAV HBI correction. Adsorbates can be estimated using a single library
2869-
for the adsorbate or a combination of a gas phase library for the
2870-
gas phase portion and an adsorption correction.
2871-
2872-
source = {'Library': String_Name_of_Library_Used,
2873-
'QM': String_of_Method_Used,
2874-
'GAV': Dictionary_of_Groups_Used,
2875-
'ADS': Dictionary_of_Adsorption_Group_Used,
2876-
}
2877-
2878-
The Dictionary_of_Groups_Used looks like
2879-
{'groupType':[List of tuples containing (Entry, Weight)]
2880-
"""
2881-
comment = species.thermo.comment
2882-
tokens = comment.split()
2883-
2884-
source = {}
2885-
2886-
if comment.startswith('Thermo library'):
2887-
# Store name of the library source, which is the 3rd token in the comments
2888-
source['Library'] = tokens[2]
2889-
2890-
elif comment.startswith('QM'):
2891-
# Store the level of the calculation, which is the 2nd token in the comments
2892-
source['QM'] = tokens[1]
2893-
2894-
elif comment.startswith('Gas phase thermo'):
2895-
# Handle adsorption correction thermo data of the following format:
2896-
# Library example
2897-
# Gas phase thermo for C(T) from Thermo library: primaryThermoLibrary.
2898-
# Adsorption correction: + Thermo group additivity estimation: adsorptionPt111(Cq*)
2899-
2900-
# GAV example
2901-
# Gas phase thermo for [CH]CC from Thermo group additivity estimation: group(Cs-CsCsHH) + group(Cs-CsHHH) + group(Cs-CsHHH) + radical(CCJ2_triplet).
2902-
# Adsorption correction: + Thermo group additivity estimation: adsorptionPt111(C=*RCR3)"
2903-
2904-
comment = comment.replace(r'\n', ' ')
2905-
comment = comment.replace('\n', ' ')
2906-
if 'Adsorption correction:' not in comment:
2907-
raise ValueError(f'adsorption correction in unrecognized format {comment}')
2908-
2909-
# Handle the gas-phase portion first
2910-
gas_comment = comment.split('Adsorption correction: + ')[0].strip()
2911-
if gas_comment.endswith('.'):
2912-
gas_comment = gas_comment[:-1] # delete the . at the end if it exists
2913-
gas_comment = gas_comment[gas_comment.find('from ', len('Gas phase thermo for ')) + len('from '):]
2914-
dummy_gas_phase_species = Species()
2915-
dummy_gas_phase_species.thermo = NASA()
2916-
dummy_gas_phase_species.thermo.comment = gas_comment
2917-
source = self.extract_source_from_comments(dummy_gas_phase_species)
2918-
2919-
# This is an adsorption correction
2920-
# comment is split into two parts: the gas phase, and the surface adsorption correction
2921-
ads_correction_comment = comment.split('Adsorption correction: +')[-1].strip()
2922-
dummy_adsorption_correction_species = Species()
2923-
dummy_adsorption_correction_species.thermo = NASA()
2924-
dummy_adsorption_correction_species.thermo.comment = ads_correction_comment
2925-
source['ADS'] = self.extract_source_from_comments(dummy_adsorption_correction_species)['GAV']
2926-
2927-
return source
2928-
2929-
# Check for group additivity contributions to the thermo in this species
2930-
2931-
# The contribution of the groups can be either additive or subtracting
2932-
# after changes to the polycyclic algorithm
2857+
def _parse_gav_groups(self, comment):
2858+
"""Extract the groups from the comment"""
2859+
groups = {}
29332860

29342861
comment = comment.replace(' + ', ' +')
29352862
comment = comment.replace(' - ', ' -')
@@ -2939,10 +2866,12 @@ def extract_source_from_comments(self, species):
29392866
# groups are still split by spaces
29402867
comment = comment.replace(')\n+', ') +')
29412868
comment = comment.replace(')\n-', ') -')
2869+
# `Thermo group additivity estimation:\nadsorptionPt111(...)` shows up in
2870+
# adsorbate comments - keep the trailing colon separated from the group token.
2871+
comment = comment.replace(':\n', ': ')
29422872
comment = comment.replace('\n', '')
29432873
tokens = comment.split(' ')
29442874

2945-
groups = {}
29462875
group_types = list(self.groups.keys())
29472876

29482877
regex = r"\((.*)\)" # only hit outermost parentheses
@@ -2970,14 +2899,180 @@ def extract_source_from_comments(self, species):
29702899

29712900
if groups:
29722901
# Indicate that group additivity is used when it is either an HBI correction
2973-
# onto a thermo library or QM value, or if the entire molecule is estimated using group additivity
2902+
# onto a thermo library or QM value, or if the entire molecule is estimated using group additivity
29742903
# Save the groups into the source dictionary
29752904

2976-
# Convert groups back into tuples
2905+
# Convert groups back into tuples
29772906
for groupType, groupDict in groups.items():
29782907
groups[groupType] = list(groupDict.items())
29792908

2980-
source['GAV'] = groups
2909+
return groups
2910+
2911+
def _parse_library_source(self, comment, library_species):
2912+
# handle the library source comment, which looks like "Thermo library: library_name"
2913+
# we then need to retrieve the specific library entry given the species
2914+
# but may have unfortunate line breaks in the middle
2915+
2916+
# trim the comment down to just the library portion so it starts with Thermo library:
2917+
split_loc = comment.find('Thermo library:')
2918+
if split_loc == -1:
2919+
raise ValueError(f"Expected 'Thermo library:' in comment, got {comment}")
2920+
2921+
comment = comment[split_loc:]
2922+
2923+
# library name is the token that comes immediately after 'Thermo library:'
2924+
assert 'Thermo library:' in comment, f"Expected 'Thermo library:' in comment, got {comment}"
2925+
tokens = comment.split()
2926+
library_name = tokens[2]
2927+
2928+
results = self.get_thermo_data_from_library(library_species, self.libraries[library_name])
2929+
if results is None:
2930+
raise DatabaseError(f"Could not find a library match for {library_species} in library {library_name}")
2931+
2932+
data, thermo_library, library_entry = results
2933+
return (library_name, library_entry)
2934+
2935+
def _parse_adsorption_correction(self, comment):
2936+
# handle the adsorption correction comment, which looks like
2937+
# "Adsorption correction: + Thermo group additivity estimation: adsorptionPt111(C-XR2CR3)"
2938+
# but may have unfortunate line breaks in the middle
2939+
2940+
# check that the number of tokens matches our expectation for an adsorption correction
2941+
# should be 8, maybe 9 if there was a weird line break
2942+
tokens = comment.split()
2943+
if len(tokens) not in [8, 9]:
2944+
raise ValueError(f"Expected 8 or 9 tokens in adsorption correction comment, got {len(tokens)}: {comment}")
2945+
2946+
ADS = self._parse_gav_groups(comment)
2947+
2948+
if len(ADS) > 1:
2949+
raise ValueError("Only adsorption corrections should be present in the adsorption correction portion of the comment. Found: {}".format(ADS))
2950+
2951+
return ADS
2952+
2953+
def extract_source_from_comments(self, species):
2954+
"""
2955+
`species`: A species object containing thermo data and thermo data comments
2956+
2957+
Parses the verbose string of comments from the thermo data of the species object,
2958+
and extracts the thermo sources.
2959+
2960+
Returns a dictionary with keys of 'Library', 'QM', 'ADS', and/or 'GAV'.
2961+
Commonly, species thermo are estimated using only one of these sources.
2962+
However, a radical can be estimated with more than one type of source, for
2963+
instance a saturated library value and a GAV HBI correction, or a QM saturated value
2964+
and a GAV HBI correction. Adsorbates can be estimated using a single library
2965+
for the adsorbate or a combination of a gas phase library for the
2966+
gas phase portion and an adsorption correction.
2967+
2968+
source = {'Library': String_Name_of_Library_Used,
2969+
'QM': String_of_Method_Used,
2970+
'GAV': Dictionary_of_Groups_Used,
2971+
'ADS': Dictionary_of_Adsorption_Group_Used,
2972+
}
2973+
2974+
The Dictionary_of_Groups_Used looks like
2975+
{'groupType':[List of tuples containing (Entry, Weight)]
2976+
"""
2977+
2978+
# TODO: solvent, electrocat, LSR
2979+
source = {}
2980+
2981+
comment = species.thermo.comment
2982+
tokens = comment.split()
2983+
2984+
ads_correction = 'Gas phase thermo' in comment and 'Adsorption correction:' in comment
2985+
library = 'Thermo library' in comment
2986+
QM = 'QM' in tokens
2987+
GAV = 'Thermo group additivity estimation:' in comment # ambiguous since ads correction looks identical to group
2988+
2989+
# the biggest thing to split on first is the adsorption correction
2990+
if ads_correction:
2991+
# The source options here are:
2992+
# (Library(gas-phase species), Adsorption correction)
2993+
# (QM(gas-phase species), Adsorption correction) <--- not really, QM is dead/sleeping
2994+
# (GAV(gas-phase species), Adsorption correction)
2995+
# (Library(gas-phase species), GAV(radical correction), Adsorption correction)
2996+
# (QM(gas-phase species), GAV(radical correction), Adsorption correction) <--- not really, QM is dead/sleeping
2997+
2998+
# split the comment into the gas phase thermo portion and the adsorption correction portion
2999+
split_loc = comment.find('Adsorption correction:')
3000+
if split_loc == -1:
3001+
raise ValueError(f"Expected 'Adsorption correction:' in comment, got {comment}")
3002+
gas_comment = comment[:split_loc].strip()
3003+
if gas_comment.endswith('.'):
3004+
gas_comment = gas_comment[:-1] # the period that closed the gas-phase sentence
3005+
ads_correction_comment = comment[split_loc:].strip()
3006+
source['ADS'] = self._parse_adsorption_correction(ads_correction_comment)
3007+
groups = self._parse_gav_groups(gas_comment)
3008+
if groups:
3009+
# Get groups first
3010+
source['GAV'] = self._parse_gav_groups(gas_comment)
3011+
3012+
if library: # (Library(gas-phase species), GAV(radical correction), Adsorption correction)
3013+
# this means the library species is the desorbed, saturated gas-phase version of the adsorbate
3014+
desorbed_gas_species = Species()
3015+
# get_desorbed_molecules already returns a list of Molecule objects
3016+
desorbed_gas_species.molecule = species.molecule[0].get_desorbed_molecules() # does deepcopy
3017+
3018+
assert desorbed_gas_species.molecule[0].is_radical(), "Method only valid for radicals."
3019+
molecule = desorbed_gas_species.molecule[0] # no need to deepcopy again since get_desorbed_molecules already does deepcopy
3020+
molecule.saturate_radicals() # note, this returns a dictionary instead of the Molecule object, but it modifies the molecule in place, so we can just ignore the returned dictionary
3021+
saturated_desorbed_gas_species = Species(molecule=[molecule])
3022+
source['Library'] = self._parse_library_source(gas_comment, saturated_desorbed_gas_species)
3023+
if QM: # (QM(gas-phase species), GAV(radical correction), Adsorption correction) <--- not really, QM is dead/sleeping
3024+
# whatever token comes immediately after 'QM' is the method used
3025+
source['QM'] = tokens[tokens.index('QM') + 1]
3026+
3027+
else:
3028+
# no groups, so this is (Library + ADS) or (QM + ADS)
3029+
if library:
3030+
# in this case, the library species is the desorbed gas-phase molecule of the adsorbate
3031+
# get_desorbed_molecules already returns a list of Molecule objects
3032+
desorbed_gas_species = Species(molecule=species.molecule[0].get_desorbed_molecules()) # does deepcopy
3033+
source['Library'] = self._parse_library_source(gas_comment, desorbed_gas_species)
3034+
if QM:
3035+
# whatever token comes immediately after 'QM' is the method used
3036+
source['QM'] = tokens[tokens.index('QM') + 1]
3037+
3038+
else:
3039+
# gas phase only, source options are:
3040+
# (Library)
3041+
# (QM)
3042+
# (GAV)
3043+
# (Library, GAV)
3044+
# (QM, GAV)
3045+
3046+
groups = self._parse_gav_groups(comment)
3047+
GAV = 'Thermo group additivity estimation:' in comment
3048+
if GAV and not groups:
3049+
raise ValueError("No groups were found in the comments but 'Thermo group additivity estimation:' was in the comment. Comment: {}".format(comment))
3050+
elif not GAV and groups:
3051+
if 'radical' not in groups.keys():
3052+
raise ValueError("Groups were found in the comments but 'Thermo group additivity estimation:' was not in the comment. Comment: {}".format(comment))
3053+
3054+
if groups:
3055+
# Get groups first
3056+
source['GAV'] = groups
3057+
if library: # (Library, GAV)
3058+
# get the saturated species for the library source
3059+
if 'radical' not in groups.keys():
3060+
raise ValueError("Method only valid for radicals, but no radical groups were found. Comment: {}".format(comment))
3061+
3062+
molecule = deepcopy(species.molecule[0])
3063+
assert molecule.is_radical(), "Method only valid for radicals."
3064+
molecule.saturate_radicals() # note, this returns a dictionary instead of the Molecule object, but it modifies the molecule in place, so we can just ignore the returned dictionary
3065+
saturated_species = Species(molecule=[molecule])
3066+
source['Library'] = self._parse_library_source(comment, saturated_species)
3067+
if QM: # (QM, GAV) <--- not really, QM is dead/sleeping
3068+
# whatever token comes immediately after 'QM' is the method used
3069+
source['QM'] = tokens[tokens.index('QM') + 1]
3070+
else: # (Library) or (QM)
3071+
if library:
3072+
source['Library'] = self._parse_library_source(comment, species)
3073+
if QM:
3074+
# whatever token comes immediately after 'QM' is the method used
3075+
source['QM'] = tokens[tokens.index('QM') + 1]
29813076

29823077
# Perform a sanity check that this molecule is estimated by at least one method
29833078
if not list(source.keys()):

0 commit comments

Comments
 (0)