@@ -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