diff --git a/molecule/chemkin.pyx b/molecule/chemkin.pyx index 8afb6b7..d69db78 100644 --- a/molecule/chemkin.pyx +++ b/molecule/chemkin.pyx @@ -178,7 +178,7 @@ def read_thermo_entry(entry, Tmin=0, Tint=0, Tmax=0): ################################################################################ -def read_kinetics_entry(entry, species_dict, Aunits, Eunits): +def read_kinetics_entry(entry, species_dict, Aunits, Aunits_surf, Eunits): """ Read a kinetics `entry` for a single reaction as loaded from a Chemkin file. The associated mapping of labels to species `species_dict` should also @@ -197,11 +197,15 @@ def read_kinetics_entry(entry, species_dict, Aunits, Eunits): # The first line contains the reaction equation and a set of # modified Arrhenius parameters reaction, third_body, kinetics, k_units, k_low_units = _read_kinetics_reaction( - line=lines[0], species_dict=species_dict, Aunits=Aunits, Eunits=Eunits) + line=lines[0], species_dict=species_dict, Aunits=Aunits, Aunits_surf=Aunits_surf, Eunits=Eunits) if len(lines) == 1 and not third_body: # If there's only one line then we know to use the high-P limit kinetics as-is - reaction.kinetics = kinetics['arrhenius high'] + if 'arrhenius high' in kinetics: + reaction.kinetics = kinetics['arrhenius high'] + elif 'surface arrhenius' in kinetics: + reaction.kinetics = kinetics['surface arrhenius'] + else: # There's more kinetics information to be read kinetics.update({ @@ -285,7 +289,13 @@ def read_kinetics_entry(entry, species_dict, Aunits, Eunits): reaction.kinetics.efficiencies = kinetics['efficiencies'] elif 'explicit reverse' in kinetics or reaction.duplicate: # it's a normal high-P reaction - the extra lines were only either REV (explicit reverse) or DUP (duplicate) - reaction.kinetics = kinetics['arrhenius high'] + if 'sticking coefficient' in kinetics: + reaction.kinetics = kinetics['sticking coefficient'] + elif 'surface arrhenius' in kinetics: + reaction.kinetics = kinetics['surface arrhenius'] + else: + reaction.kinetics = kinetics['arrhenius high'] + elif 'sticking coefficient' in kinetics: reaction.kinetics = kinetics['sticking coefficient'] elif 'surface arrhenius' in kinetics: @@ -305,7 +315,7 @@ def read_kinetics_entry(entry, species_dict, Aunits, Eunits): return reaction -def _read_kinetics_reaction(line, species_dict, Aunits, Eunits): +def _read_kinetics_reaction(line, species_dict, Aunits, Aunits_surf, Eunits): """ Parse the first line of of a Chemkin reaction entry. """ @@ -341,13 +351,14 @@ def _read_kinetics_reaction(line, species_dict, Aunits, Eunits): # Split the reaction equation into reactants and products reversible = True - reactants, products = reaction.split('=') if '<=>' in reaction: - reactants = reactants[:-1] - products = products[1:] + reactants, products = reaction.split('<=>') elif '=>' in reaction: - products = products[1:] + reactants, products = reaction.split('=>') reversible = False + else: + reactants, products = reaction.split('=') + specific_collider = None # search for a third body collider, e.g., '(+M)', '(+m)', or a specific species like '(+N2)', # matching `(+anything_other_than_ending_parenthesis)`: @@ -428,7 +439,29 @@ def _read_kinetics_reaction(line, species_dict, Aunits, Eunits): key = 'arrhenius low' if third_body else 'arrhenius high' - kinetics = { + # check if any reactants are surface species + surf_rxn = False + if any(reactant.molecule[0].contains_surface_site() for reactant in reaction.reactants): + surf_rxn = True + elif any(product.molecule[0].contains_surface_site() for product in reaction.products): + surf_rxn = True + + # check that reaction is a surface rxn. use surf arrhenius, but correct in following section + # if "STICK' is specified + if surf_rxn: + ksurfunits = Aunits_surf[n_react] + keysurf = 'surface arrhenius' + kinetics = { + keysurf: _kinetics.SurfaceArrhenius( + A=(A, ksurfunits), + n=(n, '', '+|-', dn), + Ea=(Ea, Eunits, '+|-', dEa), + T0=(1, "K"), + ) + } + + else: + kinetics = { key: _kinetics.Arrhenius( A=(A, k_units, A_uncertainty_type, dA), n=(n, '', '+|-', dn), @@ -436,6 +469,7 @@ def _read_kinetics_reaction(line, species_dict, Aunits, Eunits): T0=(1, "K"), ), } + return reaction, third_body, kinetics, k_units, k_low_units @@ -447,6 +481,13 @@ def _read_kinetics_line(line, reaction, species_dict, Eunits, kunits, klow_units line = line.upper() tokens = line.split('/') + # check if any reactants are surface species + surf_rxn = False + if any(reactant.molecule[0].contains_surface_site() for reactant in reaction.reactants): + surf_rxn = True + elif any(product.molecule[0].contains_surface_site() for product in reaction.products): + surf_rxn = True + if 'DUP' in line: # Duplicate reaction reaction.duplicate = True @@ -455,19 +496,12 @@ def _read_kinetics_line(line, reaction, species_dict, Eunits, kunits, klow_units try: k = kinetics['sticking coefficient'] except KeyError: - k = kinetics['arrhenius high'] - k = _kinetics.SurfaceArrhenius( - A=(k.A.value, kunits), - n=k.n, - Ea=k.Ea, - T0=k.T0, - ) - kinetics['surface arrhenius'] = k - del kinetics['arrhenius high'] + k = kinetics['surface arrhenius'] tokens = case_preserved_tokens[1].split() cov_dep_species = species_dict[tokens[0].strip()] - k.coverage_dependence[cov_dep_species] = {'a':float(tokens[1]), 'm':float(tokens[2]), 'E':(float(tokens[3]), Eunits)} + Ea = Quantity(float(tokens[3]), Eunits) + k.coverage_dependence[cov_dep_species] = {'a':float(tokens[1]), 'm':float(tokens[2]), 'E':Ea} elif 'LOW' in line: # Low-pressure-limit Arrhenius parameters @@ -541,7 +575,7 @@ def _read_kinetics_line(line, reaction, species_dict, Eunits, kunits, klow_units tokens2 = tokens[1].split() chebyshev.degreeT = int(float(tokens2[0].strip())) chebyshev.degreeP = int(float(tokens2[1].strip())) - chebyshev.coeffs = np.zeros((chebyshev.degreeT, chebyshev.degreeP), np.float64) + chebyshev.coeffs = np.zeros((chebyshev.degreeT, chebyshev.degreeP), float) # There may be some coefficients on this first line kinetics['chebyshev coefficients'].extend( [float(t.strip()) for t in tokens2[2:]]) @@ -572,15 +606,15 @@ def _read_kinetics_line(line, reaction, species_dict, Eunits, kunits, klow_units logging.info("Ignoring explicit reverse rate for reaction {0}".format(reaction)) elif line.strip() == 'STICK': - # Convert what we thought was Arrhenius into StickingCoefficient - k = kinetics['arrhenius high'] + # Convert what we thought was a surface arrhenius into StickingCoefficient + k = kinetics['surface arrhenius'] kinetics['sticking coefficient'] = _kinetics.StickingCoefficient( A=k.A.value, n=k.n, Ea=k.Ea, T0=k.T0, ) - del kinetics['arrhenius high'] + del kinetics['surface arrhenius'] else: # Assume a list of collider efficiencies @@ -724,7 +758,7 @@ def read_reaction_comments(reaction, comments, read=True): raise ChemkinError('Unexpected species identifier {0} encountered in flux pairs ' 'for reaction {1}.'.format(prod_str, reaction)) reaction.pairs.append((reactant, product)) - assert len(reaction.pairs) == max(len(reaction.reactants), len(reaction.products)) + #assert len(reaction.pairs) == max(len(reaction.reactants), len(reaction.products)) elif isinstance(reaction, TemplateReaction) and 'rate rule ' in line: bracketed_rule = tokens[-1] @@ -917,7 +951,7 @@ def load_transport_file(path, species_dict, skip_missing_species=False): continue species = species_dict[label] species.transport_data = TransportData( - shapeIndex=int(data[0]), + shapeIndex=int(float(data[0])), sigma=(float(data[2]), 'angstrom'), epsilon=(float(data[1]), 'K'), dipoleMoment=(float(data[3]), 'De'), @@ -928,18 +962,17 @@ def load_transport_file(path, species_dict, skip_missing_species=False): def load_chemkin_file(path, dictionary_path=None, transport_path=None, read_comments=True, thermo_path=None, - use_chemkin_names=False, check_duplicates=True, generate_resonance_structures=True): + use_chemkin_names=False, check_duplicates=True, generate_resonance_structures=True, + surface_path=False): """ Load a Chemkin input file located at `path` on disk to `path`, returning lists of the species and reactions in the Chemkin file. The 'thermo_path' point to a separate thermo file, or, if 'None' is specified, the function will look for the thermo database within the chemkin mechanism file. If `generate_resonance_structures` is True (default if omitted) then resonance isomers for each species are generated. + If `surface path` is specified, the gas and surface species and reactions will be combined """ - species_list = [] species_dict = {} - species_aliases = {} - reaction_list = [] # If the dictionary path is given, then read it and generate Molecule objects # You need to append an additional adjacency list for nonreactive species, such @@ -947,51 +980,71 @@ def load_chemkin_file(path, dictionary_path=None, transport_path=None, read_comm # HTML output. if dictionary_path: species_dict = load_species_dictionary(dictionary_path, generate_resonance_structures=generate_resonance_structures) + + def parse_file(path): + """ + helper function for parsing input file + """ + sp_list = [] + sp_aliases = {} + rxn_list = [] - with open(path, 'r') as f: - previous_line = f.tell() - line0 = f.readline() - while line0 != '': - line = remove_comment_from_line(line0)[0] - line = line.strip() - - if 'SPECIES' in line.upper(): - # Unread the line (we'll re-read it in readReactionBlock()) - f.seek(previous_line) - read_species_block(f, species_dict, species_aliases, species_list) - - elif 'SITE' in line.upper(): - # Unread the line (we'll re-read it in readReactionBlock()) - f.seek(previous_line) - read_species_block(f, species_dict, species_aliases, species_list) - - elif 'THERM' in line.upper() and thermo_path is None: - # Skip this if a thermo file is specified - # Unread the line (we'll re-read it in read_thermo_block()) - f.seek(previous_line) - read_thermo_block(f, species_dict) - - elif 'REACTIONS' in line.upper(): - # Reactions section - # Unread the line (we'll re-read it in readReactionBlock()) - f.seek(previous_line) - reaction_list = read_reactions_block(f, species_dict, read_comments=read_comments) - + with open(path, 'r') as f: previous_line = f.tell() line0 = f.readline() + while line0 != '': + line = remove_comment_from_line(line0)[0] + line = line.strip() + + if 'SPECIES' in line.upper(): + # Unread the line (we'll re-read it in readReactionBlock()) + f.seek(previous_line) + read_species_block(f, species_dict, sp_aliases, sp_list) + + elif 'SITE' in line.upper(): + # Unread the line (we'll re-read it in readReactionBlock()) + f.seek(previous_line) + read_species_block(f, species_dict, sp_aliases, sp_list) + + elif 'THERM' in line.upper() and thermo_path is None: + # Skip this if a thermo file is specified + # Unread the line (we'll re-read it in read_thermo_block()) + f.seek(previous_line) + read_thermo_block(f, species_dict) + + elif 'REACTIONS' in line.upper(): + # Reactions section + # Unread the line (we'll re-read it in readReactionBlock()) + f.seek(previous_line) + rxn_list = read_reactions_block(f, species_dict, read_comments=read_comments) + + previous_line = f.tell() + line0 = f.readline() + return sp_list, species_dict, sp_aliases, rxn_list + + + # gas + species_list, species_dict, species_aliases, reaction_list = parse_file(path) + if surface_path: + surfsp_list, surfsp_dict, surfsp_aliases, surfrxn_list = parse_file(surface_path) + species_list.extend(surfsp_list) + species_dict.update(surfsp_dict) + species_aliases.update(surfsp_aliases) + reaction_list.extend(surfrxn_list) # Read in the thermo data from the thermo file if thermo_path: with open(thermo_path, 'r') as f: - line0 = f.readline() + line0 = None while line0 != '': - line = remove_comment_from_line(line0)[0] - line = line.strip() + previous_line = f.tell() + line0 = f.readline() + line = remove_comment_from_line(line0)[0].strip() if 'THERM' in line.upper(): - f.seek(-len(line0), 1) + f.seek(previous_line) read_thermo_block(f, species_dict) break - line0 = f.readline() + # Index the reactions now to have identical numbering as in Chemkin index = 0 for reaction in reaction_list: @@ -1156,20 +1209,16 @@ def read_species_block(f, species_dict, species_aliases, species_list): if token_upper == 'END': break - site_token = token.split('/')[0] - if site_token.upper() == 'SDEN': + species_name = token.split('/')[0] # CHO*/2/ indicates an adsorbate CHO* taking 2 surface sites + if species_name.upper() == 'SDEN': # SDEN/4.1e-9/ indicates surface site density continue # TODO actually read in the site density processed_tokens.append(token) - if token in species_dict: - logging.debug("Re-using species {0} already in species_dict".format(token)) - species = species_dict[token] - elif site_token in species_dict: - logging.debug("Re-using species {0} already in species_dict".format(site_token)) - species = species_dict[site_token] + if species_name in species_dict: + species = species_dict[species_name] else: - species = Species(label=token) - species_dict[token] = species + species = Species(label=species_name) + species_dict[species_name] = species species_list.append(species) @@ -1292,6 +1341,7 @@ def read_reactions_block(f, species_dict, read_comments=True): energy_units = 'cal/mol' molecule_units = 'moles' volume_units = 'cm3' + area_units = 'cm2' time_units = 's' line = f.readline() @@ -1314,25 +1364,6 @@ def read_reactions_block(f, species_dict, read_comments=True): energy_units = unit else: raise ChemkinError('Unknown unit type "{0}"'.format(unit)) - - elif len(tokens) > 0 and tokens[0].lower() == 'unit:': - # RMG-Java kinetics library file - warnings.warn("The RMG-Java kinetic library files are" - " no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - found = True - while 'reactions:' not in line.lower(): - line = f.readline() - line = remove_comment_from_line(line)[0] - line = line.strip() - - if 'A:' in line or 'E:' in line: - units = line.split()[1] - if 'A:' in line: - molecule_units, volume_units, time_units = units.lower().split( - '/') # Assume this is a 3-tuple: moles or molecules, volume, time - elif 'E:' in line: - energy_units = units.lower() else: line = f.readline() @@ -1352,6 +1383,7 @@ def read_reactions_block(f, species_dict, read_comments=True): elif molecule_units == 'moles' or molecule_units == 'mole': molecule_units = 'mol' volume_units = {'cm3': 'cm', 'm3': 'm'}[volume_units] + area_units = {'cm2': 'cm', 'm2': 'm'}[area_units] if energy_units == 'kcal/mole': energy_units = 'kcal/mol' elif energy_units == 'cal/mole': @@ -1372,6 +1404,13 @@ def read_reactions_block(f, species_dict, read_comments=True): '{0}^6/({1}^2*{2})'.format(volume_units, molecule_units, time_units), # Third-order '{0}^9/({1}^3*{2})'.format(volume_units, molecule_units, time_units), # Fourth-order ] + + Aunits_surf = [ + '', # Zeroth-order + 's^-1'.format(time_units), # First-order + '{0}^2/({1}*{2})'.format(area_units, molecule_units, time_units), # Second-order + '{0}^4/({1}^2*{2})'.format(area_units, molecule_units, time_units), # Third-order + ] Eunits = energy_units kinetics_list = [] @@ -1419,12 +1458,6 @@ def read_reactions_block(f, species_dict, read_comments=True): # True for Chemkin files generated from RMG-Py kinetics_list.pop(0) comments_list.pop(-1) - elif kinetics_list[0] == '' and comments_list[0] == '': - # True for Chemkin files generated from RMG-Java - warnings.warn("RMG-Java loading is no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - kinetics_list.pop(0) - comments_list.pop(0) else: # In reality, comments can occur anywhere in the Chemkin # file (e.g. either or both of before and after the @@ -1442,7 +1475,7 @@ def read_reactions_block(f, species_dict, read_comments=True): reaction_list = [] for kinetics, comments in zip(kinetics_list, comments_list): try: - reaction = read_kinetics_entry(kinetics, species_dict, Aunits, Eunits) + reaction = read_kinetics_entry(kinetics, species_dict, Aunits, Aunits_surf, Eunits) reaction = read_reaction_comments(reaction, comments, read=read_comments) except ChemkinError as e: if "Skip reaction!" in str(e): @@ -1454,30 +1487,6 @@ def read_reactions_block(f, species_dict, read_comments=True): return reaction_list -################################################################################ - - -def save_html_file(path, read_comments=True): - """ - Save an output HTML file from the contents of a RMG-Java output folder - """ - warnings.warn("RMG-Java loading is no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - from molecule.rmg.model import CoreEdgeReactionModel - #from molecule.rmg.output import save_output_html - chemkin_path = os.path.join(path, 'chemkin', 'chem.inp') - dictionary_path = os.path.join(path, 'RMG_Dictionary.txt') - model = CoreEdgeReactionModel() - model.core.species, model.core.reactions = load_chemkin_file(chemkin_path, dictionary_path, - read_comments=read_comments) - output_path = os.path.join(path, 'output.html') - species_path = os.path.join(path, 'species') - if not os.path.isdir(species_path): - os.makedirs(species_path) - #save_output_html(output_path, model) - -################################################################################ - def get_species_identifier(species): """ @@ -1497,14 +1506,11 @@ def get_species_identifier(species): if species.index == -1: # No index present -- probably not in RMG job # In this case just return the label (if the right size) - if len(label) > 0 and not re.search(r'[^A-Za-z0-9\-_,\(\)\*#]+', label): - if len(label) <= 10: - return label - elif len(label) <= 15: - #logging.warning('Species label {0} is longer than 10 characters and may exceed chemkin string limit'.format(label)) + if len(label) > 0 and not re.search(r'[^A-Za-z0-9\-_,\(\)\*#.:\[\]]+', label): + if len(label) <= 16: return label else: - logging.warning('Species label is longer than 15 characters and will break CHEMKIN 2.0') + logging.warning('Species label is longer than 16 characters and will break CHEMKIN 2.0') return label else: # try the chemical formula if the species label is not present @@ -1517,17 +1523,17 @@ def get_species_identifier(species): # (at the expense of the current label or formula if need be) # First try to use the label and index - # The label can only contain alphanumeric characters, and -()*#_, - if len(label) > 0 and species.index >= 0 and not re.search(r'[^A-Za-z0-9\-_,\(\)\*#]+', label): + # The label can only contain alphanumeric characters, and -()*#_,.:[] + if len(label) > 0 and species.index >= 0 and not re.search(r'[^A-Za-z0-9\-_,\(\)\*#.:\[\]]+', label): name = '{0}({1:d})'.format(label, species.index) - if len(name) <= 10: + if len(name) <= 16: return name # Next try the chemical formula if len(species.molecule) > 0: # Try the chemical formula name = '{0}({1:d})'.format(species.molecule[0].get_formula(), species.index) - if len(name) <= 10: + if len(name) <= 16: if 'obs' in label: # For MBSampledReactor, keep observed species tag return name + '_obs' @@ -1541,7 +1547,7 @@ def get_species_identifier(species): name = 'SX({0:d})'.format(species.index) else: name = 'S({0:d})'.format(species.index) - if len(name) <= 10: + if len(name) <= 16: if 'obs' in label: # For MBSampledReactor, keep observed species tag return name + '_obs' @@ -1671,7 +1677,7 @@ def write_reaction_string(reaction, java_library=False): if kinetics is None: reaction_string = ' + '.join([get_species_identifier(reactant) for reactant in reaction.reactants]) - reaction_string += ' => ' if not reaction.reversible else ' = ' + reaction_string += ' <=> ' if reaction.reversible else ' => ' reaction_string += ' + '.join([get_species_identifier(product) for product in reaction.products]) return reaction_string @@ -1680,48 +1686,25 @@ def write_reaction_string(reaction, java_library=False): 'that support different reaction orders for the Low and High pressures limits. ' 'You should revise reaction {0}'.format(reaction.label)) - if java_library: - warnings.warn("Writing RMG-Java format is no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - third_body = '' - if kinetics.is_pressure_dependent(): - if (isinstance(kinetics, _kinetics.ThirdBody) and - not isinstance(kinetics, (_kinetics.Lindemann, _kinetics.Troe))): - third_body = ' + M' - elif isinstance(kinetics, _kinetics.PDepArrhenius): - third_body = '' - elif isinstance(kinetics, _kinetics.Chebyshev): - third_body = '' - else: - third_body = ' (+{0})'.format( - get_species_identifier(reaction.specific_collider)) if reaction.specific_collider else ' (+M)' - - reaction_string = ' + '.join([get_species_identifier(reactant) for reactant in reaction.reactants]) - reaction_string += third_body - reaction_string += ' = ' if reaction.reversible else ' => ' - reaction_string += ' + '.join([get_species_identifier(product) for product in reaction.products]) - reaction_string += third_body - - else: - third_body = '' - if kinetics.is_pressure_dependent(): - if (isinstance(kinetics, _kinetics.ThirdBody) and - not isinstance(kinetics, (_kinetics.Lindemann, _kinetics.Troe))): - third_body = '+M' - elif isinstance(kinetics, (_kinetics.PDepArrhenius, _kinetics.MultiPDepArrhenius)): - third_body = '' - else: - third_body = '(+{0})'.format( - get_species_identifier(reaction.specific_collider)) if reaction.specific_collider else '(+M)' + third_body = '' + if kinetics.is_pressure_dependent(): + if (isinstance(kinetics, _kinetics.ThirdBody) and + not isinstance(kinetics, (_kinetics.Lindemann, _kinetics.Troe))): + third_body = '+M' + elif isinstance(kinetics, (_kinetics.PDepArrhenius, _kinetics.MultiPDepArrhenius)): + third_body = '' + else: + third_body = '(+{0})'.format( + get_species_identifier(reaction.specific_collider)) if reaction.specific_collider else '(+M)' - reaction_string = '+'.join([get_species_identifier(reactant) for reactant in reaction.reactants]) - reaction_string += third_body - reaction_string += '=' if reaction.reversible else '=>' - reaction_string += '+'.join([get_species_identifier(product) for product in reaction.products]) - reaction_string += third_body + reaction_string = '+'.join([get_species_identifier(reactant) for reactant in reaction.reactants]) + reaction_string += third_body + reaction_string += '<=>' if reaction.reversible else '=>' + reaction_string += '+'.join([get_species_identifier(product) for product in reaction.products]) + reaction_string += third_body if len(reaction_string) > 52: - logging.warning("Chemkin reaction string {0!r} is too long for Chemkin 2!".format(reaction_string)) + logging.debug("Chemkin reaction string '%s' is too long for Chemkin 2!", reaction_string) return reaction_string ################################################################################ @@ -1875,12 +1858,6 @@ def write_kinetics_entry(reaction, species_list, verbose=True, java_library=Fals # Print dummy values that Chemkin parses but ignores string += '{0:<9.3e} {1:<9.3f} {2:<9.3f}'.format(1, 0, 0) - if java_library: - warnings.warn("RMG-Java libraries are no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - # Assume uncertainties are zero (when parsing from chemkin), may need to adapt later - string += '{0:<9.1f} {1:<9.1f} {2:<9.1f}'.format(0, 0, 0) - string += '\n' if getattr(kinetics, 'coverage_dependence', None): @@ -1888,7 +1865,7 @@ def write_kinetics_entry(reaction, species_list, verbose=True, java_library=Fals for species, cov_params in kinetics.coverage_dependence.items(): label = get_species_identifier(species) string += f' COV / {label:<41} ' - string += f"{cov_params['a'].value:<9.3g} {cov_params['m'].value:<9.3g} {cov_params['E'].value_si/4184.:<9.3f} /\n" + string += f"{cov_params['a']:<9.3g} {cov_params['m']:<9.3g} {cov_params['E'].value_si/4184.:<9.3f} /\n" if isinstance(kinetics, (_kinetics.ThirdBody, _kinetics.Lindemann, _kinetics.Troe)): # Write collider efficiencies @@ -2233,48 +2210,6 @@ def save_chemkin_surface_file(path, species, reactions, verbose=True, check_for_ _chemkin_reaction_count = None -def save_java_kinetics_library(path, species, reactions): - """ - Save the reaction files for a RMG-Java kinetics library: pdepreactions.txt - and reactions.txt given a list of reactions, with species.txt containing the - RMG-Java formatted dictionary. - """ - warnings.warn("Java kinetics libararies are no longer supported and may be" \ - "removed in version 2.3.", DeprecationWarning) - # Check for duplicate - mark_duplicate_reactions(reactions) - - f = open(os.path.join(path, 'reactions.txt'), 'w') - f2 = open(os.path.join(path, 'pdepreactions.txt'), 'w') - - # Headers - f.write('Unit:\n') - f.write('A: mol/cm3/s\n') - f.write('E: kcal/mol\n') - f.write('\n') - f.write('Reactions:\n') - f.write('\n') - - f2.write('Unit:\n') - f2.write('A: mol/cm3/s\n') - f2.write('E: kcal/mol\n') - f2.write('\n') - f2.write('Reactions:\n') - f2.write('\n') - - for rxn in reactions: - if rxn.kinetics.is_pressure_dependent(): - f2.write(write_kinetics_entry(rxn, species_list=species, verbose=False, java_library=True)) - f2.write('\n') - else: - f.write(write_kinetics_entry(rxn, species_list=species, verbose=False, java_library=True)) - f.write('\n') - f.close() - f2.close() - - save_species_dictionary(os.path.join(path, 'species.txt'), species, old_style=True) - - def save_chemkin(reaction_model, path, verbose_path, dictionary_path=None, transport_path=None, save_edge_species=False): """ diff --git a/molecule/constants.pxd b/molecule/constants.pxd index 292ec66..491c98a 100644 --- a/molecule/constants.pxd +++ b/molecule/constants.pxd @@ -25,4 +25,4 @@ # # ############################################################################### -cdef double pi, Na, kB, R, h, hbar, c, e, m_e, m_p, m_n, amu, a0, E_h +cdef double pi, Na, kB, R, h, hbar, c, e, m_e, m_p, m_n, amu, a0, E_h, F diff --git a/molecule/constants.py b/molecule/constants.py index f5da192..803ead3 100644 --- a/molecule/constants.py +++ b/molecule/constants.py @@ -110,6 +110,12 @@ #: :math:`\pi = 3.14159 \ldots` pi = float(math.pi) +#: Faradays Constant F in C/mol +F = 96485.3321233100184 + +#: Vacuum permittivity +epsilon_0 = 8.8541878128 + ################################################################################ # Cython does not automatically place module-level variables into the module @@ -130,4 +136,6 @@ 'm_n': m_n, 'm_p': m_p, 'pi': pi, + 'F': F, + 'epsilon_0': epsilon_0, }) diff --git a/molecule/data/base.py b/molecule/data/base.py index 4bf4c6b..03f1e8b 100644 --- a/molecule/data/base.py +++ b/molecule/data/base.py @@ -42,6 +42,7 @@ from molecule.data.reference import Reference, Article, Book, Thesis from molecule.exceptions import DatabaseError, InvalidAdjacencyListError from molecule.kinetics.uncertainties import RateUncertainty +from molecule.kinetics.arrhenius import ArrheniusChargeTransfer, ArrheniusChargeTransferBM from molecule.molecule import Molecule, Group @@ -228,6 +229,8 @@ def load(self, path, local_context=None, global_context=None): local_context['shortDesc'] = self.short_desc local_context['longDesc'] = self.long_desc local_context['RateUncertainty'] = RateUncertainty + local_context['ArrheniusChargeTransfer'] = ArrheniusChargeTransfer + local_context['ArrheniusChargeTransferBM'] = ArrheniusChargeTransferBM local_context['metal'] = self.metal local_context['site'] = self.site local_context['facet'] = self.facet @@ -236,13 +239,17 @@ def load(self, path, local_context=None, global_context=None): local_context[key] = value # Process the file - f = open(path, 'r') + with open(path, 'r') as f: + content = f.read() try: - exec(f.read(), global_context, local_context) - except Exception: - logging.error('Error while reading database {0!r}.'.format(path)) + exec(content, global_context, local_context) + except Exception as e: + logging.exception(f'Error while reading database file {path}.') + line_number = e.__traceback__.tb_next.tb_lineno + logging.error(f'Error occurred at or near line {line_number} of {path}.') + lines = content.splitlines() + logging.error(f'Line: {lines[line_number - 1]}') raise - f.close() # Extract the database metadata self.name = local_context['name'] diff --git a/molecule/data/kinetics/common.py b/molecule/data/kinetics/common.py index 1b0beb9..ae32212 100644 --- a/molecule/data/kinetics/common.py +++ b/molecule/data/kinetics/common.py @@ -222,9 +222,55 @@ def independent_ids(): for species in input_species: species.generate_resonance_structures(keep_isomorphic=True) +def check_for_same_reactants(reactants): + """ + Given a list reactants, 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. + + Returns a tuple containing the modified reactants list, and an integer containing the number of identical reactants in the reactants list. + + """ + + same_reactants = 0 + if len(reactants) == 2: + if reactants[0] is reactants[1]: + reactants[1] = reactants[1].copy(deep=True) + same_reactants = 2 + elif reactants[0].is_isomorphic(reactants[1]): + same_reactants = 2 + elif len(reactants) == 3: + same_01 = reactants[0] is reactants[1] + same_02 = reactants[0] is reactants[2] + if same_01 and same_02: + same_reactants = 3 + reactants[1] = reactants[1].copy(deep=True) + reactants[2] = reactants[2].copy(deep=True) + elif same_01: + same_reactants = 2 + reactants[1] = reactants[1].copy(deep=True) + elif same_02: + same_reactants = 2 + reactants[2] = reactants[2].copy(deep=True) + elif reactants[1] is reactants[2]: + same_reactants = 2 + reactants[2] = reactants[2].copy(deep=True) + else: + same_01 = reactants[0].is_isomorphic(reactants[1]) + same_02 = reactants[0].is_isomorphic(reactants[2]) + if same_01 and same_02: + same_reactants = 3 + elif same_01 or same_02: + same_reactants = 2 + elif reactants[1].is_isomorphic(reactants[2]): + same_reactants = 2 + elif len(reactants) > 3: + raise ValueError('Cannot check for duplicate reactants if provided number of reactants is greater than 3. ' + 'Got: {} reactants'.format(len(reactants))) + + return reactants, same_reactants def find_degenerate_reactions(rxn_list, same_reactants=None, template=None, kinetics_database=None, - kinetics_family=None, save_order=False): + kinetics_family=None, save_order=False, resonance=True): """ Given a list of Reaction objects, this method combines degenerate reactions and increments the reaction degeneracy value. For multiple @@ -250,6 +296,7 @@ def find_degenerate_reactions(rxn_list, same_reactants=None, template=None, kine kinetics_database (KineticsDatabase, optional): provide a KineticsDatabase instance for calculating degeneracy kinetics_family (KineticsFamily, optional): provide a KineticsFamily instance for calculating degeneracy save_order (bool, optional): reset atom order after performing atom isomorphism + resonance (bool, optional): whether to consider resonance when computing degeneracy Returns: Reaction list with degenerate reactions combined with proper degeneracy values @@ -340,7 +387,7 @@ def find_degenerate_reactions(rxn_list, same_reactants=None, template=None, kine from molecule.data.rmg import get_db family = get_db('kinetics').families[rxn.family] if not family.own_reverse: - rxn.degeneracy = family.calculate_degeneracy(rxn) + rxn.degeneracy = family.calculate_degeneracy(rxn, resonance=resonance) return rxn_list diff --git a/molecule/data/kinetics/database.py b/molecule/data/kinetics/database.py index 9e3be12..2241a52 100644 --- a/molecule/data/kinetics/database.py +++ b/molecule/data/kinetics/database.py @@ -37,18 +37,22 @@ import molecule.constants as constants from molecule.data.base import LogicNode from molecule.data.kinetics.common import ensure_species, generate_molecule_combos, \ - find_degenerate_reactions, ensure_independent_atom_ids + find_degenerate_reactions, ensure_independent_atom_ids, \ + check_for_same_reactants from molecule.data.kinetics.family import KineticsFamily from molecule.data.kinetics.library import LibraryReaction, KineticsLibrary from molecule.exceptions import DatabaseError from molecule.kinetics import Arrhenius, ArrheniusEP, ThirdBody, Lindemann, Troe, \ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, \ Chebyshev, KineticsData, StickingCoefficient, \ - StickingCoefficientBEP, SurfaceArrhenius, SurfaceArrheniusBEP, ArrheniusBM + StickingCoefficientBEP, SurfaceArrhenius, SurfaceArrheniusBEP, \ + ArrheniusBM, SurfaceChargeTransfer, KineticsModel, Marcus, \ + ArrheniusChargeTransfer, ArrheniusChargeTransferBM +from molecule.kinetics.uncertainties import RateUncertainty from molecule.molecule import Molecule, Group from molecule.reaction import Reaction, same_species_lists from molecule.species import Species - +from molecule.data.solvation import SoluteData, SoluteTSData, SoluteTSDiffData ################################################################################ @@ -61,12 +65,14 @@ def __init__(self): self.recommended_families = {} self.families = {} self.libraries = {} + self.external_library_labels = {} self.library_order = [] # a list of tuples in the format ('library_label', LibraryType), # where LibraryType is set to either 'Reaction Library' or 'Seed'. self.local_context = { 'KineticsData': KineticsData, 'Arrhenius': Arrhenius, 'ArrheniusEP': ArrheniusEP, + 'ArrheniusChargeTransfer': ArrheniusChargeTransfer, 'MultiArrhenius': MultiArrhenius, 'MultiPDepArrhenius': MultiPDepArrhenius, 'PDepArrhenius': PDepArrhenius, @@ -78,8 +84,16 @@ def __init__(self): 'StickingCoefficientBEP': StickingCoefficientBEP, 'SurfaceArrhenius': SurfaceArrhenius, 'SurfaceArrheniusBEP': SurfaceArrheniusBEP, + 'SurfaceChargeTransfer': SurfaceChargeTransfer, 'R': constants.R, - 'ArrheniusBM': ArrheniusBM + 'ArrheniusBM': ArrheniusBM, + 'ArrheniusChargeTransferBM': ArrheniusChargeTransferBM, + 'SoluteData': SoluteData, + 'SoluteTSData': SoluteTSData, + 'SoluteTSDiffData': SoluteTSDiffData, + 'KineticsModel': KineticsModel, + 'Marcus': Marcus, + 'RateUncertainty': RateUncertainty, } self.global_context = {} @@ -226,17 +240,18 @@ def load_libraries(self, path, libraries=None): The `path` points to the folder of kinetics libraries in the database, and the libraries should be in files like :file:`/.py`. """ - + self.external_library_labels = dict() if libraries is not None: for library_name in libraries: library_file = os.path.join(path, library_name, 'reactions.py') if os.path.exists(library_name): library_file = os.path.join(library_name, 'reactions.py') - short_library_name = os.path.split(library_name)[-1] + short_library_name = os.path.basename(library_name.rstrip(os.path.sep)) logging.info(f'Loading kinetics library {short_library_name} from {library_name}...') library = KineticsLibrary(label=short_library_name) library.load(library_file, self.local_context, self.global_context) self.libraries[library.label] = library + self.external_library_labels[library_name] = library.label elif os.path.exists(library_file): logging.info(f'Loading kinetics library {library_name} from {library_file}...') library = KineticsLibrary(label=library_name) @@ -251,10 +266,18 @@ def load_libraries(self, path, libraries=None): self.library_order = [] for (root, dirs, files) in os.walk(os.path.join(path)): for f in files: - name, ext = os.path.splitext(f) - if ext.lower() == '.py': + if f.lower() == 'reactions.py': library_file = os.path.join(root, f) - label = os.path.dirname(library_file)[len(path) + 1:] + dirname = os.path.dirname(library_file) + if dirname == path: + label = os.path.basename(dirname) + else: + label = os.path.relpath(dirname, path) + + if not label: + logging.warning(f"Empty label for {library_file}. Using 'default'.") + label = "default" + logging.info(f'Loading kinetics library {label} from {library_file}...') library = KineticsLibrary(label=label) try: @@ -428,7 +451,7 @@ def generate_reactions(self, reactants, products=None, only_families=None, reson if only_families is None: reaction_list.extend(self.generate_reactions_from_libraries(reactants, products)) reaction_list.extend(self.generate_reactions_from_families(reactants, products, - only_families=None, resonance=resonance)) + only_families=only_families, resonance=resonance)) return reaction_list def generate_reactions_from_libraries(self, reactants, products=None): @@ -488,43 +511,10 @@ def generate_reactions_from_families(self, reactants, products=None, only_famili 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 if isinstance(reactants, tuple): reactants = list(reactants) - same_reactants = 0 - if len(reactants) == 2: - if reactants[0] is reactants[1]: - reactants[1] = reactants[1].copy(deep=True) - same_reactants = 2 - elif reactants[0].is_isomorphic(reactants[1]): - same_reactants = 2 - elif len(reactants) == 3: - same_01 = reactants[0] is reactants[1] - same_02 = reactants[0] is reactants[2] - if same_01 and same_02: - same_reactants = 3 - reactants[1] = reactants[1].copy(deep=True) - reactants[2] = reactants[2].copy(deep=True) - elif same_01: - same_reactants = 2 - reactants[1] = reactants[1].copy(deep=True) - elif same_02: - same_reactants = 2 - reactants[2] = reactants[2].copy(deep=True) - elif reactants[1] is reactants[2]: - same_reactants = 2 - reactants[2] = reactants[2].copy(deep=True) - else: - same_01 = reactants[0].is_isomorphic(reactants[1]) - same_02 = reactants[0].is_isomorphic(reactants[2]) - if same_01 and same_02: - same_reactants = 3 - elif same_01 or same_02: - same_reactants = 2 - elif reactants[1].is_isomorphic(reactants[2]): - same_reactants = 2 + + reactants, same_reactants = check_for_same_reactants(reactants) # Label reactant atoms for proper degeneracy calculation (cannot be in tuple) ensure_independent_atom_ids(reactants, resonance=resonance) @@ -537,7 +527,8 @@ def generate_reactions_from_families(self, reactants, products=None, only_famili prod_resonance=resonance)) # Calculate reaction degeneracy - reaction_list = find_degenerate_reactions(reaction_list, same_reactants, kinetics_database=self) + reaction_list = find_degenerate_reactions(reaction_list, same_reactants, kinetics_database=self, + resonance=resonance) # Add reverse attribute to families with ownReverse to_delete = [] for i, rxn in enumerate(reaction_list): @@ -658,7 +649,7 @@ def get_forward_reaction_for_family_entry(self, entry, family, thermo_database): elif len(reverse) == 1 and len(forward) == 0: # The reaction is in the reverse direction # First fit Arrhenius kinetics in that direction - T_data = 1000.0 / np.arange(0.5, 3.301, 0.1, np.float64) + T_data = 1000.0 / np.arange(0.5, 3.301, 0.1, float) k_data = np.zeros_like(T_data) for i in range(T_data.shape[0]): k_data[i] = entry.data.get_rate_coefficient(T_data[i]) / reaction.get_equilibrium_constant(T_data[i]) diff --git a/molecule/data/kinetics/depository.py b/molecule/data/kinetics/depository.py index a9e01f5..625ded6 100644 --- a/molecule/data/kinetics/depository.py +++ b/molecule/data/kinetics/depository.py @@ -35,6 +35,7 @@ from molecule.data.base import Database, Entry, DatabaseError from molecule.data.kinetics.common import save_entry +from molecule.kinetics import SurfaceChargeTransfer, SurfaceArrheniusBEP from molecule.reaction import Reaction @@ -60,7 +61,8 @@ def __init__(self, pairs=None, depository=None, family=None, - entry=None + entry=None, + electrons=0, ): Reaction.__init__(self, index=index, @@ -72,7 +74,8 @@ def __init__(self, transition_state=transition_state, duplicate=duplicate, degeneracy=degeneracy, - pairs=pairs + pairs=pairs, + electrons=electrons, ) self.depository = depository self.family = family @@ -104,12 +107,34 @@ def get_source(self): """ return self.depository.label + def apply_solvent_correction(self, solvent): + """ + apply kinetic solvent correction + """ + from molecule.data.rmg import get_db + solvation_database = get_db('solvation') + solvent_data = solvation_database.get_solvent_data(solvent) + solute_data = self.kinetics.solute + correction = solvation_database.get_solvation_correction(solute_data, solvent_data) + dHR = 0.0 + dSR = 0.0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc) + spc_correction = solvation_database.get_solvation_correction(spc_solute_data, solvent_data) + dHR += spc_correction.enthalpy + dSR += spc_correction.entropy + + dH = correction.enthalpy-dHR + dA = np.exp((correction.entropy-dSR)/constants.R) + self.kinetics.Ea.value_si += dH + self.kinetics.A.value_si *= dA + self.kinetics.comment += "\nsolvation correction raised barrier by {0} kcal/mol and prefactor by factor of {1}".format(dH/4184.0,dA) ################################################################################ class KineticsDepository(Database): """ - A class for working with an RMG kinetics depository. Each depository + A class for working with an RMG kinetics depository. Each depository corresponds to a reaction family (a :class:`KineticsFamily` object). Each entry in a kinetics depository involves a reaction defined either by a real reactant and product species (as in a kinetics library). @@ -187,6 +212,9 @@ def load(self, path, local_context=None, global_context=None): ''.format(product, self.label)) # Same comment about molecule vs species objects as above. rxn.products.append(species_dict[product]) + + if isinstance(entry.data, (SurfaceChargeTransfer, SurfaceArrheniusBEP)): + rxn.electrons = entry.data.electrons.value if not rxn.is_balanced(): raise DatabaseError('Reaction {0} in kinetics depository {1} was not balanced! Please reformulate.' diff --git a/molecule/data/kinetics/family.py b/molecule/data/kinetics/family.py index a557e60..5c54456 100644 --- a/molecule/data/kinetics/family.py +++ b/molecule/data/kinetics/family.py @@ -36,6 +36,7 @@ import multiprocessing as mp import os.path import random +import math import re import warnings from collections import OrderedDict @@ -48,14 +49,15 @@ # from molecule.constraints import fails_species_constraints from molecule.data.base import Database, Entry, LogicNode, LogicOr, ForbiddenStructures, get_all_combinations from molecule.data.kinetics.common import save_entry, find_degenerate_reactions, generate_molecule_combos, \ - ensure_independent_atom_ids + ensure_independent_atom_ids, check_for_same_reactants from molecule.data.kinetics.depository import KineticsDepository from molecule.data.kinetics.groups import KineticsGroups from molecule.data.kinetics.rules import KineticsRules from molecule.exceptions import ActionError, DatabaseError, InvalidActionError, KekulizationError, KineticsError, \ ForbiddenStructureException, UndeterminableKineticsError from molecule.kinetics import Arrhenius, SurfaceArrhenius, SurfaceArrheniusBEP, StickingCoefficient, \ - StickingCoefficientBEP, ArrheniusBM + StickingCoefficientBEP, ArrheniusBM, SurfaceChargeTransfer, ArrheniusChargeTransfer, \ + ArrheniusChargeTransferBM, KineticsModel, Marcus from molecule.kinetics.uncertainties import RateUncertainty, rank_accuracy_map from molecule.molecule import Bond, GroupBond, Group, Molecule from molecule.molecule.atomtype import ATOMTYPES @@ -63,6 +65,8 @@ from molecule.species import Species # from molecule.tools.uncertainty import KineticParameterUncertainty from molecule.molecule.fragment import Fragment +import molecule.constants as constants +from molecule.data.solvation import SoluteData, add_solute_data, SoluteTSData, to_soluteTSdata ################################################################################ @@ -77,7 +81,7 @@ class TemplateReaction(Reaction): Attribute Type Description =============== ========================= ===================================== `family` ``str`` The kinetics family that the reaction was created from. - `estimator` ``str`` Whether the kinetics came from rate rules or group additivity. + `estimator` ``str`` The name of the kinetic estimator; currently only rate rules is supported. `reverse` :class:`TemplateReaction` The reverse reaction, for families that are their own reverse. `is_forward` ``bool`` Whether the reaction was generated in the forward direction of the family. `labeled_atoms` ``dict`` Keys are 'reactants' or 'products', values are dictionaries. @@ -102,6 +106,7 @@ def __init__(self, estimator=None, reverse=None, is_forward=None, + electrons=0, ): Reaction.__init__(self, index=index, @@ -115,6 +120,7 @@ def __init__(self, degeneracy=degeneracy, pairs=pairs, is_forward=is_forward, + electrons=electrons ) self.family = family self.template = template @@ -140,7 +146,8 @@ def __reduce__(self): self.template, self.estimator, self.reverse, - self.is_forward + self.is_forward, + self.electrons )) def __repr__(self): @@ -162,6 +169,7 @@ def __repr__(self): if self.pairs is not None: string += 'pairs={0}, '.format(self.pairs) if self.family: string += "family='{}', ".format(self.family) if self.template: string += "template={}, ".format(self.template) + if self.electrons: string += "electrons={}, ".format(self.electrons) if self.comment != '': string += 'comment={0!r}, '.format(self.comment) string = string[:-2] + ')' return string @@ -195,6 +203,7 @@ def copy(self): other.transition_state = deepcopy(self.transition_state) other.duplicate = self.duplicate other.pairs = deepcopy(self.pairs) + other.electrons = self.electrons # added for TemplateReaction information other.family = self.family @@ -205,6 +214,86 @@ def copy(self): return other + def apply_solvent_correction(self, solvent): + """ + apply kinetic solvent correction in this case the parameters are dGTSsite instead of GTS + """ + from molecule.data.rmg import get_db + solvation_database = get_db('solvation') + solvent_data = solvation_database.get_solvent_data(solvent) + + + if isinstance(self.kinetics, Marcus): + solvent_struct = solvation_database.get_solvent_structure(solvent)[0] + solv_solute_data = solvation_database.get_solute_data(solvent_struct.copy(deep=True)) + Rsolv = math.pow((75 * solv_solute_data.V / constants.pi / constants.Na), + (1.0 / 3.0)) / 100 + Rtot = 0.0 + Ner = 0 + Nep = 0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) + spc_solute_data.set_mcgowan_volume(spc) + R = math.pow((75 * spc_solute_data.V / constants.pi / constants.Na), + (1.0 / 3.0)) / 100 + Rtot += R + Ner += spc.get_net_charge() + for spc in self.products: + Nep += spc.get_net_charge() + + Rtot += Rsolv #radius of reactants plus first solvation shell + self.lmbd_o = constants.Na*(constants.e*(Nep-Ner))**2/(8.0*constants.pi*constants.epsilon_0*Rtot)*(1.0/solvent_data.n**2 - 1.0/solvent_data.eps) + return + + site_data = to_soluteTSdata(self.kinetics.solute) + + #compute x from gas phase + GR = 0.0 + GP = 0.0 + for reactant in self.reactants: + try: + GR += reactant.get_free_energy(298.0) + except Exception: + logging.error("Problem with reactant {!r} in reaction {!s}".format(reactant, self)) + raise + for product in self.products: + try: + GP += product.get_free_energy(298.0) + except Exception: + logging.error("Problem with product {!r} in reaction {!s}".format(reactant, self)) + raise + + GTS = self.kinetics.Ea.value_si + GR + + #x = abs(GTS - GR) / (abs(GP - GTS) + abs(GR - GTS)) + dGrxn = GP-GR + if dGrxn > 0: + x = 1.0 + else: + x = 0.0 + + dHR = 0.0 + dSR = 0.0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) + spc_soluteTS_data = to_soluteTSdata(spc_solute_data) + site_data += spc_soluteTS_data*(1.0-x) + spc_correction = solvation_database.get_solvation_correction(spc_solute_data, solvent_data) + dHR += spc_correction.enthalpy + dSR += spc_correction.entropy + + for spc in self.products: + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data += spc_solute_data*x + + dGTS,dHTS = site_data.calculate_corrections(solvent_data) + dSTS = (dHTS - dGTS)/298.0 + + dH = dHTS-dHR + dA = np.exp((dSTS-dSR)/constants.R) + self.kinetics.Ea.value_si += dH + self.kinetics.A.value_si *= dA + self.kinetics.comment += "\nsolvation correction raised barrier by {0} kcal/mol and prefactor by factor of {1}".format(dH/4184.0,dA) ################################################################################ @@ -260,6 +349,10 @@ def get_reverse(self): other.add_action(['GAIN_RADICAL', action[1], action[2]]) elif action[0] == 'GAIN_RADICAL': other.add_action(['LOSE_RADICAL', action[1], action[2]]) + elif action[0] == 'GAIN_CHARGE': + other.add_action(['LOSE_CHARGE', action[1], action[2]]) + elif action[0] == 'LOSE_CHARGE': + other.add_action(['GAIN_CHARGE', action[1], action[2]]) elif action[0] == 'LOSE_PAIR': other.add_action(['GAIN_PAIR', action[1], action[2]]) elif action[0] == 'GAIN_PAIR': @@ -309,7 +402,7 @@ def _apply(self, struct, forward, unique): if info < 1: raise InvalidActionError('Attempted to change a nonexistent bond.') # If we do not have a bond, it might be because we are trying to change a vdW bond - # Lets see if one of that atoms is a surface site, + # Lets see if one of that atoms is a surface site, # If we have a surface site, we will make a single bond, then change it by info - 1 is_vdW_bond = False for atom in (atom1, atom2): @@ -383,7 +476,7 @@ def _apply(self, struct, forward, unique): atom1.apply_action(['BREAK_BOND', label1, info, label2]) atom2.apply_action(['BREAK_BOND', label1, info, label2]) - elif action[0] in ['LOSE_RADICAL', 'GAIN_RADICAL']: + elif action[0] in ['LOSE_RADICAL', 'GAIN_RADICAL', 'LOSE_CHARGE', 'GAIN_CHARGE']: label, change = action[1:] change = int(change) @@ -401,6 +494,10 @@ def _apply(self, struct, forward, unique): atom.apply_action(['GAIN_RADICAL', label, 1]) elif (action[0] == 'LOSE_RADICAL' and forward) or (action[0] == 'GAIN_RADICAL' and not forward): atom.apply_action(['LOSE_RADICAL', label, 1]) + elif (action[0] == 'LOSE_CHARGE' and forward) or (action[0] == 'GAIN_CHARGE' and not forward): + atom.apply_action(['LOSE_CHARGE', label, 1]) + elif (action[0] == 'GAIN_CHARGE' and forward) or (action[0] == 'LOSE_CHARGE' and not forward): + atom.apply_action(['GAIN_CHARGE', label, 1]) elif action[0] in ['LOSE_PAIR', 'GAIN_PAIR']: @@ -445,8 +542,8 @@ def apply_reverse(self, struct, unique=True): class KineticsFamily(Database): """ - A class for working with an RMG kinetics family: a set of reactions with - similar chemistry, and therefore similar reaction rates. The attributes + A class for working with an RMG kinetics family: a set of reactions with + similar chemistry, and therefore similar reaction rates. The attributes are: =================== =============================== ======================== @@ -513,172 +610,9 @@ def __init__(self, def __repr__(self): return ''.format(self.label) - def load_old(self, path): - """ - Load an old-style RMG kinetics group additivity database from the - location `path`. - """ - warnings.warn("The old kinetics databases are no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - self.label = os.path.basename(path) - self.name = self.label - - self.groups = KineticsGroups(label='{0}/groups'.format(self.label)) - self.groups.name = self.groups.label - try: - self.groups.load_old_dictionary(os.path.join(path, 'dictionary.txt'), pattern=True) - except Exception: - logging.error('Error while reading old kinetics family dictionary from {0!r}.'.format(path)) - raise - try: - self.groups.load_old_tree(os.path.join(path, 'tree.txt')) - except Exception: - logging.error('Error while reading old kinetics family tree from {0!r}.'.format(path)) - raise - - # The old kinetics groups use rate rules (not group additivity values), - # so we can't load the old rateLibrary.txt - - # Load the reaction recipe - try: - self.load_old_template(os.path.join(path, 'reactionAdjList.txt')) - except Exception: - logging.error('Error while reading old kinetics family template/recipe from {0!r}.'.format(path)) - raise - # Construct the forward and reverse templates - reactants = [self.groups.entries[label] for label in self.forward_template.reactants] - if self.own_reverse: - self.forward_template = Reaction(reactants=reactants, products=reactants) - self.reverse_template = None - else: - products = self.generate_product_template(reactants) - self.forward_template = Reaction(reactants=reactants, products=products) - self.reverse_template = Reaction(reactants=reactants, products=products) - - self.groups.reactant_num = len(self.forward_template.reactants) - - # Load forbidden structures if present - try: - if os.path.exists(os.path.join(path, 'forbiddenGroups.txt')): - self.forbidden = ForbiddenStructures().load_old(os.path.join(path, 'forbiddenGroups.txt')) - except Exception: - logging.error('Error while reading old kinetics family forbidden groups from {0!r}.'.format(path)) - raise - - entries = self.groups.top[:] - for entry in self.groups.top: - entries.extend(self.groups.descendants(entry)) - for index, entry in enumerate(entries): - entry.index = index + 1 - - self.rules = KineticsRules(label='{0}/rules'.format(self.label)) - self.rules.name = self.rules.label - try: - self.rules.load_old(path, self.groups, - num_labels=max(len(self.forward_template.reactants), len(self.groups.top))) - except Exception: - logging.error('Error while reading old kinetics family rules from {0!r}.'.format(path)) - raise - self.depositories = {} - - return self - - def load_old_template(self, path): - """ - Load an old-style RMG reaction family template from the location `path`. - """ - warnings.warn("The old kinetics databases are no longer supported and" - " may be removed in version 2.3.", DeprecationWarning) - self.forward_template = Reaction(reactants=[], products=[]) - self.forward_recipe = ReactionRecipe() - self.own_reverse = False - - ftemp = None - # Process the template file - try: - ftemp = open(path, 'r') - for line in ftemp: - line = line.strip() - if len(line) > 0 and line[0] == '(': - # This is a recipe action line - tokens = line.split() - action = [tokens[1]] - action.extend(tokens[2][1:-1].split(',')) - self.forward_recipe.add_action(action) - elif 'thermo_consistence' in line: - self.own_reverse = True - elif 'reverse' in line: - self.reverse = line.split(':')[1].strip() - elif '->' in line: - # This is the template line - tokens = line.split() - at_arrow = False - for token in tokens: - if token == '->': - at_arrow = True - elif token != '+' and not at_arrow: - self.forward_template.reactants.append(token) - elif token != '+' and at_arrow: - self.forward_template.products.append(token) - except IOError as e: - logging.exception('Database template file "' + e.filename + '" not found.') - raise - finally: - if ftemp: ftemp.close() - - def save_old(self, path): - """ - Save the old RMG kinetics groups to the given `path` on disk. - """ - warnings.warn("The old kinetics databases are no longer supported and" - " may be removed in version 2.3.", DeprecationWarning) - if not os.path.exists(path): os.mkdir(path) - - self.groups.save_old_dictionary(os.path.join(path, 'dictionary.txt')) - self.groups.save_old_tree(os.path.join(path, 'tree.txt')) - # The old kinetics groups use rate rules (not group additivity values), - # so we can't save the old rateLibrary.txt - self.save_old_template(os.path.join(path, 'reactionAdjList.txt')) - # Save forbidden structures if present - if self.forbidden is not None: - self.forbidden.save_old(os.path.join(path, 'forbiddenGroups.txt')) - - self.rules.save_old(path, self) - - def save_old_template(self, path): - """ - Save an old-style RMG reaction family template from the location `path`. - """ - warnings.warn("The old kinetics databases are no longer supported and" - " may be removed in version 2.3.", DeprecationWarning) - f_temp = open(path, 'w') - - # Write the template - f_temp.write('{0} -> {1}\n'.format( - ' + '.join([entry.label for entry in self.forward_template.reactants]), - ' + '.join([entry.label for entry in self.forward_template.products]), - )) - f_temp.write('\n') - - # Write the reaction type and reverse name - if self.own_reverse: - f_temp.write('thermo_consistence\n') - else: - f_temp.write('forward\n') - f_temp.write('reverse: {0}\n'.format(self.reverse)) - f_temp.write('\n') - - # Write the reaction recipe - f_temp.write('Actions 1\n') - for index, action in enumerate(self.forward_recipe.actions): - f_temp.write('({0}) {1:<15} {{{2}}}\n'.format(index + 1, action[0], ','.join(action[1:]))) - f_temp.write('\n') - - f_temp.close() - def distribute_tree_distances(self): """ - fills in nodal_distance (the distance between an entry and its parent) + Fills in nodal_distance (the distance between an entry and its parent) if not already entered with the value from tree_distances associated with the tree the entry comes from """ @@ -700,11 +634,11 @@ def distribute_tree_distances(self): def load(self, path, local_context=None, global_context=None, depository_labels=None): """ Load a kinetics database from a file located at `path` on disk. - + If `depository_labels` is a list, eg. ['training','PrIMe'], then only those depositories are loaded, and they are searched in that order when generating kinetics. - + If depository_labels is None then load 'training' first then everything else. If depository_labels is not None then load in the order specified in depository_labels. """ @@ -721,6 +655,8 @@ def load(self, path, local_context=None, global_context=None, depository_labels= local_context['reactantNum'] = None local_context['productNum'] = None local_context['autoGenerated'] = False + local_context['allowChargedSpecies'] = False + local_context['electrons'] = 0 self.groups = KineticsGroups(label='{0}/groups'.format(self.label)) logging.debug("Loading kinetics family groups from {0}".format(os.path.join(path, 'groups.py'))) Database.load(self.groups, os.path.join(path, 'groups.py'), local_context, global_context) @@ -733,6 +669,8 @@ def load(self, path, local_context=None, global_context=None, depository_labels= self.product_num = local_context.get('productNum', None) self.auto_generated = local_context.get('autoGenerated', False) + self.allow_charged_species = local_context.get('allowChargedSpecies', False) + self.electrons = local_context.get('electrons', 0) if self.reactant_num: self.groups.reactant_num = self.reactant_num @@ -749,6 +687,9 @@ def load(self, path, local_context=None, global_context=None, depository_labels= self.reverse = local_context.get('reverse', None) self.reversible = True if local_context.get('reversible', None) is None else local_context.get('reversible', None) self.forward_template.products = self.generate_product_template(self.forward_template.reactants) + for entry in self.forward_template.products: + if isinstance(entry.item,Group): + entry.item.update() if self.reversible: self.reverse_template = Reaction(reactants=self.forward_template.products, products=self.forward_template.reactants) @@ -797,7 +738,7 @@ def load(self, path, local_context=None, global_context=None, depository_labels= # depository and add them to the RMG rate rules by default: depository_labels = ['training'] if depository_labels: - # If there are depository labels, load them in the order specified, but + # If there are depository labels, load them in the order specified, but # append the training reactions unless the user specifically declares it not # to be included with a '!training' flag if '!training' not in depository_labels: @@ -836,7 +777,8 @@ def load_recipe(self, actions): for action in actions: action[0] = action[0].upper() valid_actions = [ - 'CHANGE_BOND', 'FORM_BOND', 'BREAK_BOND', 'GAIN_RADICAL', 'LOSE_RADICAL', 'GAIN_PAIR', 'LOSE_PAIR' + 'CHANGE_BOND', 'FORM_BOND', 'BREAK_BOND', 'GAIN_RADICAL', 'LOSE_RADICAL', + 'GAIN_CHARGE', 'LOSE_CHARGE', 'GAIN_PAIR', 'LOSE_PAIR' ] if action[0] not in valid_actions: raise InvalidActionError('Action {0} is not a recognized action. ' @@ -863,12 +805,12 @@ def save_training_reactions(self, reactions, reference=None, reference_type='', rank=3): """ This function takes a list of reactions appends it to the training reactions file. It ignores the existence of - duplicate reactions. - - The rank for each new reaction's kinetics is set to a default value of 3 unless the user specifies differently + duplicate reactions. + + The rank for each new reaction's kinetics is set to a default value of 3 unless the user specifies differently for those reactions. - - For each entry, the long description is imported from the kinetics comment. + + For each entry, the long description is imported from the kinetics comment. """ if not isinstance(reference, list): @@ -971,7 +913,7 @@ def save_training_reactions(self, reactions, reference=None, reference_type='', def save(self, path): """ - Save the current database to the file at location `path` on disk. + Save the current database to the file at location `path` on disk. """ self.save_groups(os.path.join(path, 'groups.py')) self.rules.save(os.path.join(path, 'rules.py')) @@ -988,7 +930,7 @@ def save_depository(self, depository, path): def save_groups(self, path): """ - Save the current database to the file at location `path` on disk. + Save the current database to the file at location `path` on disk. """ entries = self.groups.get_entries_to_save() @@ -1024,10 +966,16 @@ def save_groups(self, path): f.write('reactantNum = {0}\n\n'.format(self.reactant_num)) if self.product_num is not None: f.write('productNum = {0}\n\n'.format(self.product_num)) - + if self.auto_generated is not None: f.write('autoGenerated = {0}\n\n'.format(self.auto_generated)) + if self.allow_charged_species: + f.write('allowChargedSpecies = {0}\n\n'.format(self.allow_charged_species)) + + if self.electrons != 0: + f.write('electrons = {0}\n\n'.format(self.electrons)) + # Write the recipe f.write('recipe(actions=[\n') for action in self.forward_recipe.actions: @@ -1148,14 +1096,14 @@ def generate_product_template(self, reactants0): def has_rate_rule(self, template): """ - Return ``True`` if a rate rule with the given `template` currently + Return ``True`` if a rate rule with the given `template` currently exists, or ``False`` otherwise. """ return self.rules.has_rule(template) def get_rate_rule(self, template): """ - Return the rate rule with the given `template`. Raises a + Return the rate rule with the given `template`. Raises a :class:`ValueError` if no corresponding entry exists. """ entry = self.rules.get_rule(template) @@ -1239,6 +1187,21 @@ def add_rules_from_training(self, thermo_database=None, train_indices=None): Tmax=deepcopy(data.Tmax), coverage_dependence=deepcopy(data.coverage_dependence), ) + elif isinstance(data, SurfaceChargeTransfer): + for reactant in entry.item.reactants: + # Clear atom labels to avoid effects on thermo generation, ok because this is a deepcopy + reactant_copy = reactant.copy(deep=True) + reactant_copy.molecule[0].clear_labeled_atoms() + reactant_copy.generate_resonance_structures() + reactant.thermo = thermo_database.get_thermo_data(reactant_copy, training_set=True) + for product in entry.item.products: + product_copy = product.copy(deep=True) + product_copy.molecule[0].clear_labeled_atoms() + product_copy.generate_resonance_structures() + product.thermo = thermo_database.get_thermo_data(product_copy, training_set=True) + V = data.V0.value_si + dGrxn = entry.item._get_free_energy_of_charge_transfer_reaction(298,V) + data = data.to_surface_charge_transfer_bep(dGrxn,0.0) else: raise NotImplementedError("Unexpected training kinetics type {} for {}".format(type(data), entry)) @@ -1267,7 +1230,9 @@ def add_rules_from_training(self, thermo_database=None, train_indices=None): for entry in reverse_entries: tentries[entry.index].item.is_forward = False - assert isinstance(entry.data, Arrhenius) + if not isinstance(entry.data, Arrhenius): + print(self.label) + assert False data = deepcopy(entry.data) data.change_t0(1) # Estimate the thermo for the reactants and products @@ -1348,7 +1313,7 @@ def add_rules_from_training(self, thermo_database=None, train_indices=None): def get_root_template(self): """ Return the root template for the reaction family. Most of the time this - is the top-level nodes of the tree (as stored in the + is the top-level nodes of the tree (as stored in the :class:`KineticsGroups` object), but there are a few exceptions (e.g. R_Recombination). """ @@ -1513,22 +1478,23 @@ def apply_recipe(self, reactant_structures, forward=True, unique=True, relabel_a product_num = self.product_num or len(template.products) # Split product structure into multiple species if necessary - if (isinstance(product_structure, Group) and self.auto_generated and self.label in ["Intra_R_Add_Endocyclic","Intra_R_Add_Exocyclic"]): + if self.auto_generated and isinstance(reactant_structures[0],Group) and self.product_num == 1: product_structures = [product_structure] else: product_structures = product_structure.split() - # Make sure we've made the expected number of products - if product_num != len(product_structures): - # We have a different number of products than expected by the template. - # By definition this means that the template is not a match, so - # we return None to indicate that we could not generate the product - # structures - # We need to think this way in order to distinguish between - # intermolecular and intramolecular versions of reaction families, - # which will have very different kinetics - # Unfortunately this may also squash actual errors with malformed - # reaction templates - return None + + # Make sure we've made the expected number of products + if product_num != len(product_structures): + # We have a different number of products than expected by the template. + # By definition this means that the template is not a match, so + # we return None to indicate that we could not generate the product + # structures + # We need to think this way in order to distinguish between + # intermolecular and intramolecular versions of reaction families, + # which will have very different kinetics + # Unfortunately this may also squash actual errors with malformed + # reaction templates + return None # Remove vdW bonds for struct in product_structures: @@ -1546,15 +1512,20 @@ def apply_recipe(self, reactant_structures, forward=True, unique=True, relabel_a struc.update() reactant_net_charge += struc.get_net_charge() + + is_molecule = True for struct in product_structures: # If product structures are Molecule objects, update their atom types # If product structures are Group objects and the reaction is in certain families # (families with charged substances), the charge of structures will be updated if isinstance(struct, Molecule): - struct.update(sort_atoms=not self.save_order) - elif isinstance(struct, Fragment): - struct.update() + struct.update_charge() + if isinstance(struct, Fragment): + struct.update() + else: + struct.update(sort_atoms=not self.save_order) elif isinstance(struct, Group): + is_molecule = False struct.reset_ring_membership() if label in ['1,2_insertion_co', 'r_addition_com', 'co_disproportionation', 'intra_no2_ono_conversion', 'lone_electron_pair_bond', @@ -1562,20 +1533,25 @@ def apply_recipe(self, reactant_structures, forward=True, unique=True, relabel_a struct.update_charge() else: raise TypeError('Expecting Molecule or Group object, not {0}'.format(struct.__class__.__name__)) - product_net_charge += struc.get_net_charge() - if reactant_net_charge != product_net_charge: + product_net_charge += struct.get_net_charge() + + + if self.electrons < 0: + if forward: + reactant_net_charge += self.electrons + else: + product_net_charge += self.electrons + elif self.electrons > 0: + if forward: + product_net_charge -= self.electrons + else: + reactant_net_charge -= self.electrons + + if reactant_net_charge != product_net_charge and is_molecule: logging.debug( 'The net charge of the reactants {0} differs from the net charge of the products {1} in reaction ' 'family {2}. Not generating this reaction.'.format(reactant_net_charge, product_net_charge, self.label)) return None - # The following check should be removed once RMG can process charged species - # This is applied only for :class:Molecule (not for :class:Group which is allowed to have a nonzero net charge) - if any([structure.get_net_charge() for structure in reactant_structures + product_structures]) \ - and isinstance(struc, Molecule): - logging.debug( - 'A net charged species was formed when reacting {0} to form {1} in reaction family {2}. Not generating ' - 'this reaction.'.format(reactant_net_charge, product_net_charge, self.label)) - return None # If there are two product structures, place the one containing '*1' first if len(product_structures) == 2: @@ -1686,10 +1662,10 @@ def _generate_product_structures(self, reactant_structures, maps, forward, relab def is_molecule_forbidden(self, molecule): """ Return ``True`` if the molecule is forbidden in this family, or - ``False`` otherwise. + ``False`` otherwise. """ - # check family-specific forbidden structures + # check family-specific forbidden structures if self.forbidden is not None and self.forbidden.is_molecule_forbidden(molecule): return True @@ -1721,8 +1697,17 @@ def _create_reaction(self, reactants, products, is_forward): reversible=self.reversible, family=self.label, is_forward=is_forward, + electrons = self.electrons ) + if not self.allow_charged_species: + for spc in (reaction.reactants + reaction.products): + if spc.get_net_charge() != 0: + return None + + if not reaction.is_balanced(): + return None + # Store the labeled atoms so we can recover them later # (e.g. for generating reaction pairs and templates) for key, species_list in zip(['reactants', 'products'], [reaction.reactants, reaction.products]): @@ -1733,7 +1718,7 @@ def _create_reaction(self, reactants, products, is_forward): def _match_reactant_to_template(self, reactant, template_reactant): """ - Return a complete list of the mappings if the provided reactant + Return a complete list of the mappings if the provided reactant matches the provided template reactant, or an empty list if not. """ @@ -1904,65 +1889,37 @@ def add_reverse_attribute(self, rxn, react_non_reactive=True): rxn.reverse = reactions[0] return True - def calculate_degeneracy(self, reaction): + def calculate_degeneracy(self, reaction, resonance=True): """ For a `reaction` with `Molecule` or `Species` objects given in the direction in which - the kinetics are defined, compute the reaction-path degeneracy. + the kinetics are defined, compute the reaction-path degeneracy. Can specify whether to consider resonance. - This method by default adjusts for double counting of identical reactants. - This should only be adjusted once per reaction. To not adjust for + This method by default adjusts for double counting of identical reactants. + This should only be adjusted once per reaction. To not adjust for identical reactants (since you will be reducing them later in the algorithm), add `ignoreSameReactants= True` to this method. """ # Check if the reactants are the same # If they refer to the same memory address, then make a deep copy so # they can be manipulated independently + if reaction.is_charge_transfer_reaction(): + # Not implemented yet for charge transfer reactions + return 1 reactants = reaction.reactants - same_reactants = 0 - if len(reactants) == 2: - if reactants[0] is reactants[1]: - reactants[1] = reactants[1].copy(deep=True) - same_reactants = 2 - elif reactants[0].is_isomorphic(reactants[1]): - same_reactants = 2 - elif len(reactants) == 3: - same_01 = reactants[0] is reactants[1] - same_02 = reactants[0] is reactants[2] - if same_01 and same_02: - same_reactants = 3 - reactants[1] = reactants[1].copy(deep=True) - reactants[2] = reactants[2].copy(deep=True) - elif same_01: - same_reactants = 2 - reactants[1] = reactants[1].copy(deep=True) - elif same_02: - same_reactants = 2 - reactants[2] = reactants[2].copy(deep=True) - elif reactants[1] is reactants[2]: - same_reactants = 2 - reactants[2] = reactants[2].copy(deep=True) - else: - same_01 = reactants[0].is_isomorphic(reactants[1]) - same_02 = reactants[0].is_isomorphic(reactants[2]) - if same_01 and same_02: - same_reactants = 3 - elif same_01 or same_02: - same_reactants = 2 - elif reactants[1].is_isomorphic(reactants[2]): - same_reactants = 2 + reactants, same_reactants = check_for_same_reactants(reactants) # Label reactant atoms for proper degeneracy calculation - ensure_independent_atom_ids(reactants, resonance=True) + ensure_independent_atom_ids(reactants, resonance=resonance) molecule_combos = generate_molecule_combos(reactants) reactions = [] for combo in molecule_combos: reactions.extend(self._generate_reactions(combo, products=reaction.products, forward=True, - react_non_reactive=True)) + prod_resonance=resonance, react_non_reactive=True)) # remove degenerate reactions reactions = find_degenerate_reactions(reactions, same_reactants, template=reaction.template, - kinetics_family=self) + kinetics_family=self, resonance=resonance) # log issues if len(reactions) != 1: @@ -2009,7 +1966,7 @@ def _generate_reactions(self, reactants, products=None, forward=True, prod_reson rxn_list = [] - # Wrap each reactant in a list if not already done (this is done to + # Wrap each reactant in a list if not already done (this is done to # allow for passing multiple resonance structures for each molecule) # This also makes a copy of the reactants list so we don't modify the # original @@ -2323,7 +2280,7 @@ def generate_products_and_reactions(order): if not forward and ('adsorption' in self.label.lower() or 'eleyrideal' in self.label.lower()): # Desorption should have desorbed something (else it was probably bidentate) # so delete reactions that don't make a gas-phase desorbed product - # Eley-Rideal reactions should have one gas-phase product in the reverse direction + # Eley-Rideal reactions should have one gas-phase product in the reverse direction # Determine how many surf reactants we expect based on the template n_surf_expected = len([r for r in self.forward_template.reactants if r.item.contains_surface_site()]) @@ -2393,7 +2350,7 @@ def get_reaction_pairs(self, reaction): """ pairs = [] if len(reaction.reactants) == 1 or len(reaction.products) == 1: - # When there is only one reactant (or one product), it is paired + # When there is only one reactant (or one product), it is paired # with each of the products (reactants) for reactant in reaction.reactants: for product in reaction.products: @@ -2559,28 +2516,25 @@ def get_reaction_template(self, reaction): def get_kinetics_for_template(self, template, degeneracy=1, method='rate rules'): """ Return an estimate of the kinetics for a reaction with the given - `template` and reaction-path `degeneracy`. There are two possible methods - to use: 'group additivity' (new possible RMG-Py behavior) and 'rate rules' (old - RMG-Java behavior, and default RMG-Py behavior). + `template` and reaction-path `degeneracy`. There is currently only one method to use: + 'rate rules' (old RMG-Java behavior, and default RMG-Py behavior). Group additivity was removed in August 2023. Returns a tuple (kinetics, entry): If it's estimated via 'rate rules' and an exact match is found in the tree, then the entry is returned as the second element of the tuple. - But if an average is used, or the 'group additivity' method, then the tuple - returned is (kinetics, None). + But if an average is used, then the tuple returned is (kinetics, None). + """ - if method.lower() == 'group additivity': - return self.estimate_kinetics_using_group_additivity(template, degeneracy), None - elif method.lower() == 'rate rules': + if method.lower() == 'rate rules': return self.estimate_kinetics_using_rate_rules(template, degeneracy) # This returns kinetics and entry data else: raise ValueError('Invalid value "{0}" for method parameter; ' - 'should be "group additivity" or "rate rules".'.format(method)) + 'currently only "rate rules" is supported.'.format(method)) def get_kinetics_from_depository(self, depository, reaction, template, degeneracy): """ Search the given `depository` in this kinetics family for kinetics - for the given `reaction`. Returns a list of all of the matching + for the given `reaction`. Returns a list of all of the matching kinetics, the corresponding entries, and ``True`` if the kinetics match the forward direction or ``False`` if they match the reverse direction. @@ -2621,8 +2575,8 @@ def _select_best_kinetics(self, kinetics_list): def get_kinetics(self, reaction, template_labels, degeneracy=1, estimator='', return_all_kinetics=True): """ Return the kinetics for the given `reaction` by searching the various - depositories as well as generating a result using the user-specified `estimator` - of either 'group additivity' or 'rate rules'. Unlike + depositories as well as generating a result using the user-specified `estimator`. + Currently, only 'rate rules' is a supported estimator. Unlike the regular :meth:`get_kinetics()` method, this returns a list of results, with each result comprising of @@ -2631,7 +2585,7 @@ def get_kinetics(self, reaction, template_labels, degeneracy=1, estimator='', re 3. the entry - this will be `None` if from a template estimate 4. is_forward a boolean denoting whether the matched entry is in the same direction as the inputted reaction. This will always be True if using - rates rules or group additivity. This can be `True` or `False` if using + rates rules. This can be `True` or `False` if using a depository If return_all_kinetics==False, only the first (best?) matching kinetics is returned. @@ -2652,7 +2606,9 @@ def get_kinetics(self, reaction, template_labels, degeneracy=1, estimator='', re for kinetics, entry, is_forward in kinetics_list0: kinetics_list.append([kinetics, depository, entry, is_forward]) - # If estimator type of rate rules or group additivity is given, retrieve the kinetics. + # If estimator type of rate rules is given, retrieve the kinetics. + # TODO: Since group additivity was removed, this logic can be condensed into just 1 branch. + if estimator: try: kinetics, entry = self.get_kinetics_for_template(template, degeneracy, method=estimator) @@ -2665,7 +2621,6 @@ def get_kinetics(self, reaction, template_labels, degeneracy=1, estimator='', re return kinetics, estimator, entry, True kinetics_list.append([kinetics, estimator, entry, True]) # If no estimation method was given, prioritize rate rule estimation. - # If returning all kinetics, add estimations from both rate rules and group additivity. else: try: kinetics, entry = self.get_kinetics_for_template(template, degeneracy, method='rate rules') @@ -2676,49 +2631,16 @@ def get_kinetics(self, reaction, template_labels, degeneracy=1, estimator='', re # If kinetics were undeterminable for rate rules estimation, do nothing. pass - try: - kinetics2, entry2 = self.get_kinetics_for_template(template, degeneracy, method='group additivity') - if not return_all_kinetics: - return kinetics2, 'group additivity', entry2, True - kinetics_list.append([kinetics2, 'group additivity', entry2, True]) - except KineticsError: - # If kinetics were undeterminable for group additivity estimation, do nothing. - pass - if not return_all_kinetics: raise UndeterminableKineticsError(reaction) return kinetics_list - def estimate_kinetics_using_group_additivity(self, template, degeneracy=1): - """ - Determine the appropriate kinetics for a reaction with the given - `template` using group additivity. - - Returns just the kinetics, or None. - """ - warnings.warn("Group additivity is no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - # Start with the generic kinetics of the top-level nodes - kinetics = None - root = self.get_root_template() - kinetics = self.get_kinetics_for_template(root) - - if kinetics is None: - # raise UndeterminableKineticsError('Cannot determine group additivity kinetics estimate for ' - # 'template "{0}".'.format(','.join([e.label for e in template]))) - return None - else: - kinetics = kinetics[0] - - # Now add in more specific corrections if possible - return self.groups.estimate_kinetics_using_group_additivity(template, kinetics, degeneracy) - def estimate_kinetics_using_rate_rules(self, template, degeneracy=1): """ Determine the appropriate kinetics for a reaction with the given `template` using rate rules. - + Returns a tuple (kinetics, entry) where `entry` is the database entry used to determine the kinetics only if it is an exact match, and is None if some averaging or use of a parent node took place. @@ -2729,8 +2651,8 @@ def estimate_kinetics_using_rate_rules(self, template, degeneracy=1): def get_reaction_template_labels(self, reaction): """ - Retrieve the template for the reaction and - return the corresponding labels for each of the + Retrieve the template for the reaction and + return the corresponding labels for each of the groups in the template. """ template = self.get_reaction_template(reaction) @@ -2743,8 +2665,8 @@ def get_reaction_template_labels(self, reaction): def retrieve_template(self, template_labels): """ - Reconstruct the groups associated with the - labels of the reaction template and + Reconstruct the groups associated with the + labels of the reaction template and return a list. """ template = [] @@ -2755,9 +2677,9 @@ def retrieve_template(self, template_labels): def get_labeled_reactants_and_products(self, reactants, products, relabel_atoms=True): """ - Given `reactants`, a list of :class:`Molecule` objects, and products, a list of - :class:`Molecule` objects, return two new lists of :class:`Molecule` objects with - atoms labeled: one for reactants, one for products. Returned molecules are totally + Given `reactants`, a list of :class:`Molecule` objects, and products, a list of + :class:`Molecule` objects, return two new lists of :class:`Molecule` objects with + atoms labeled: one for reactants, one for products. Returned molecules are totally new entities in memory so input molecules `reactants` and `products` won't be affected. If RMG cannot find appropriate labels, (None, None) will be returned. If ``relabel_atoms`` is ``True``, product atom labels of reversible families @@ -2936,7 +2858,7 @@ def add_entry(self, parent, grp, name): def _split_reactions(self, rxns, newgrp): """ divides the reactions in rxns between the new - group structure newgrp and the old structure with + group structure newgrp and the old structure with label oldlabel returns a list of reactions associated with the new group the list of reactions associated with the old group @@ -2961,14 +2883,14 @@ def _split_reactions(self, rxns, newgrp): comp.append(rxn) return new, comp, new_inds - + def reaction_matches(self, rxn, grp): rmol = rxn.reactants[0].molecule[0] for r in rxn.reactants[1:]: rmol = rmol.merge(r.molecule[0]) rmol.identify_ring_membership() return rmol.is_subgraph_isomorphic(grp, generate_initial_map=True, save_order=True) - + def eval_ext(self, parent, ext, extname, template_rxn_map, obj=None, T=1000.0): """ evaluates the objective function obj @@ -2992,15 +2914,15 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, finds the set of all extension groups to parent such that 1) the extension group divides the set of reactions under parent 2) No generalization of the extension group divides the set of reactions under parent - + We find this by generating all possible extensions of the initial group. Extensions that split reactions are added - to the list. All extensions that do not split reactions and do not create bonds are ignored + to the list. All extensions that do not split reactions and do not create bonds are ignored (although those that match every reaction are labeled so we don't search them twice). Those that match - all reactions and involve bond creation undergo this process again. - - Principle: Say you have two elementary changes to a group ext1 and ext2 if applying ext1 and ext2 results in a + all reactions and involve bond creation undergo this process again. + + Principle: Say you have two elementary changes to a group ext1 and ext2 if applying ext1 and ext2 results in a split at least one of ext1 and ext2 must result in a split - + Speed of this algorithm relies heavily on searching non bond creation dimensions once. """ out_exts = [[]] @@ -3011,7 +2933,7 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, n_splits = len(template_rxn_map[parent.label][0].reactants) iter = 0 - + while grps[iter] != []: grp = grps[iter][-1] @@ -3130,7 +3052,7 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, out_exts.append([]) grps[iter].pop() names.pop() - + for ind in ext_inds: # collect the groups to be expanded grpr, grpcr, namer, typr, indcr = exts[ind] if len(grps) == iter+1: @@ -3140,17 +3062,17 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, if first_time: first_time = False - + if grps[iter] == [] and len(grps) != iter+1 and (not (any([len(x)>0 for x in out_exts]) and iter+1 > iter_max)): iter += 1 if len(grps[iter]) > iter_item_cap: logging.error("Recursion item cap hit not splitting {0} reactions at iter {1} with {2} items".format(len(template_rxn_map[parent.label]),iter,len(grps[iter]))) iter -= 1 gave_up_split = True - + elif grps[iter] == [] and len(grps) != iter+1 and (any([len(x)>0 for x in out_exts]) and iter+1 > iter_max): logging.error("iter_max achieved terminating early") - + out = [] # compile all of the valid extensions together # may be some duplicates here, but I don't think it's currently worth identifying them @@ -3161,7 +3083,7 @@ def get_extension_edge(self, parent, template_rxn_map, obj, T, iter_max=np.inf, def extend_node(self, parent, template_rxn_map, obj=None, T=1000.0, iter_max=np.inf, iter_item_cap=np.inf): """ - Constructs an extension to the group parent based on evaluation + Constructs an extension to the group parent based on evaluation of the objective function obj """ exts, gave_up_split = self.get_extension_edge(parent, template_rxn_map, obj=obj, T=T, iter_max=iter_max, iter_item_cap=iter_item_cap) @@ -3217,10 +3139,10 @@ def extend_node(self, parent, template_rxn_map, obj=None, T=1000.0, iter_max=np. parent.item.clear_reg_dims() # this almost always solves the problem return True return False - + if gave_up_split: return False - + vals = [] for grp, grpc, name, typ, einds in exts: val, boo = self.eval_ext(parent, grp, name, template_rxn_map, obj, T) @@ -3316,7 +3238,7 @@ def extend_node(self, parent, template_rxn_map, obj=None, T=1000.0, iter_max=np. logging.error(prod.label) logging.error(prod.to_adjacency_list()) raise ValueError - + template_rxn_map[extname] = new_entries if complement: @@ -3332,21 +3254,21 @@ def generate_tree(self, rxns=None, obj=None, thermo_database=None, T=1000.0, npr """ Generate a tree by greedy optimization based on the objective function obj the optimization is done by iterating through every group and if the group has - more than one training reaction associated with it a set of potential more specific extensions - are generated and the extension that optimizing the objective function combination is chosen + more than one training reaction associated with it a set of potential more specific extensions + are generated and the extension that optimizing the objective function combination is chosen and the iteration starts over at the beginning - + additionally the tree structure is simplified on the fly by removing groups that have no kinetics data associated if their parent has no kinetics data associated and they either have only one child or have two children one of which has no kinetics data and no children (its parent becomes the parent of its only relevant child node) - + Args: rxns: List of reactions to generate tree from (if None pull the whole training set) obj: Object to expand tree from (if None uses top node) thermo_database: Thermodynamic database used for reversing training reactions T: Temperature the tree is optimized for - nprocs: Number of process for parallel tree generation + nprocs: Number of process for parallel tree generation min_splitable_entry_num: the minimum number of splitable reactions at a node in order to spawn a new process solving that node min_rxns_to_spawn: the minimum number of reactions at a node to spawn a new process solving that node @@ -3359,7 +3281,7 @@ def generate_tree(self, rxns=None, obj=None, thermo_database=None, T=1000.0, npr """ if rxns is None: rxns = self.get_training_set(thermo_database=thermo_database, remove_degeneracy=True, estimate_thermo=True, - fix_labels=True, get_reverse=True) + fix_labels=True, get_reverse=True, rxns_with_kinetics_only=True) if len(rxns) <= max_batch_size: template_rxn_map = self.get_reaction_matches(rxns=rxns, thermo_database=thermo_database, remove_degeneracy=True, @@ -3390,9 +3312,9 @@ def rxnkey(rxn): min_splitable_entry_num=min_splitable_entry_num, min_rxns_to_spawn=min_rxns_to_spawn, extension_iter_max=extension_iter_max, extension_iter_item_cap=extension_iter_item_cap) logging.error("built tree with {} nodes".format(len(list(self.groups.entries)))) - + self.auto_generated = True - + def get_rxn_batches(self, rxns, T=1000.0, max_batch_size=800, outlier_fraction=0.02, stratum_num=8): """ Breaks reactions into batches based on a modified stratified sampling scheme @@ -3495,7 +3417,7 @@ def make_tree_nodes(self, template_rxn_map=None, obj=None, T=1000.0, nprocs=0, d entries.remove(entry) else: psize = float(len(template_rxn_map[root.label])) - + logging.error(psize) mult_completed_nodes = [] # nodes containing multiple identical training reactions boo = True # if the for loop doesn't break becomes false and the while loop terminates @@ -3621,13 +3543,34 @@ def make_bm_rules_from_template_rxn_map(self, template_rxn_map, nprocs=1, Tref=1 inds = inds.tolist() revinds = [inds.index(x) for x in np.arange(len(inputs))] - pool = mp.Pool(nprocs) + if nprocs > 1: + pool = mp.Pool(nprocs) + kinetics_list = np.array(pool.map(_make_rule, inputs[inds])) + else: + kinetics_list = np.array(list(map(_make_rule, inputs[inds]))) - kinetics_list = np.array(pool.map(_make_rule, inputs[inds])) kinetics_list = kinetics_list[revinds] # fix order for i, kinetics in enumerate(kinetics_list): - if kinetics is not None: + if isinstance(kinetics, Marcus): + entry = entries[i] + st = "Marcus rule fitted to {0} training reactions at node {1}".format(len(rxnlists[i][0]), entry.label) + new_entry = Entry( + index=index, + label=entry.label, + item=self.forward_template, + data=kinetics, + rank=11, + reference=None, + short_desc=st, + long_desc=st, + ) + new_entry.data.comment = st + + self.rules.entries[entry.label].append(new_entry) + + index += 1 + elif kinetics is not None: entry = entries[i] std = kinetics.uncertainty.get_expected_log_uncertainty() / 0.398 # expected uncertainty is std * 0.398 st = "BM rule fitted to {0} training reactions at node {1}".format(len(rxnlists[i][0]), entry.label) @@ -3648,11 +3591,21 @@ def make_bm_rules_from_template_rxn_map(self, template_rxn_map, nprocs=1, Tref=1 index += 1 - def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1000.0, iters=0, random_state=1, ascend=False): + for label,entry in self.rules.entries.items(): #pull solute data from further up the tree as needed + if len(entry) == 0: + continue + entry = entry[0] + if not entry.data.solute: + ent = self.groups.entries[label] + while not self.rules.entries[ent.label][0].data.solute and ent.parent: + ent = ent.parent + entry.data.solute = self.rules.entries[ent.label][0].data.solute + + def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1000.0, iters=0, random_state=1): """ Perform K-fold cross validation on an automatically generated tree at temperature T after finding an appropriate node for kinetics estimation it will move up the tree - iters times. + iters times. Returns a dictionary mapping {rxn:Ln(k_Est/k_Train)} """ @@ -3704,44 +3657,44 @@ def cross_validate(self, folds=5, template_rxn_map=None, test_rxn_inds=None, T=1 if entry.parent: entry = entry.parent + boo = True + + while boo: + if entry.parent is None: + break + kin = self.rules.entries[entry.label][0].data + kinparent = self.rules.entries[entry.parent.label][0].data + err_parent = abs(kinparent.uncertainty.data_mean + kinparent.uncertainty.mu - kin.uncertainty.data_mean) + np.sqrt(2.0*kinparent.uncertainty.var/np.pi) + err_entry = abs(kin.uncertainty.mu) + np.sqrt(2.0*kin.uncertainty.var/np.pi) + if err_entry <= err_parent: + break + else: + entry = entry.parent + uncertainties[rxn] = self.rules.entries[entry.label][0].data.uncertainty - - if not ascend: - L = list(set(template_rxn_map[entry.label]) - set(rxns_test)) - if L != []: + + L = list(set(template_rxn_map[entry.label]) - set(rxns_test)) + + if L != []: + if isinstance(L[0].kinetics, Arrhenius): kinetics = ArrheniusBM().fit_to_reactions(L, recipe=self.forward_recipe.actions) - kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(T)) - k = kinetics.get_rate_coefficient(T) - errors[rxn] = np.log(k / krxn) + if kinetics.E0.value_si < 0.0 or len(L) == 1: + kinetics = average_kinetics([r.kinetics for r in L]) + else: + kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(298.)) else: - raise ValueError('only one piece of kinetics information in the tree?') - else: - boo = True - rlist = list(set(template_rxn_map[entry.label]) - set(rxns_test)) - kinetics = _make_rule((self.forward_recipe.actions,rlist,T,1.0e3,"",[rxn.rank for rxn in rlist])) - logging.error("determining fold rate") - c = 1 - while boo: - parent = entry.parent - if parent is None: - break - rlistparent = list(set(template_rxn_map[parent.label]) - set(rxns_test)) - kineticsparent = _make_rule((self.forward_recipe.actions,rlistparent,T,1.0e3,"",[rxn.rank for rxn in rlistparent])) - err_parent = abs(kineticsparent.uncertainty.data_mean + kineticsparent.uncertainty.mu - kinetics.uncertainty.data_mean) + np.sqrt(2.0*kineticsparent.uncertainty.var/np.pi) - err_entry = abs(kinetics.uncertainty.mu) + np.sqrt(2.0*kinetics.uncertainty.var/np.pi) - if err_entry > err_parent: - entry = entry.parent - kinetics = kineticsparent - logging.error("recursing {}".format(c)) - c += 1 + kinetics = ArrheniusChargeTransferBM().fit_to_reactions(L, recipe=self.forward_recipe.actions) + if kinetics.E0.value_si < 0.0 or len(L) == 1: + kinetics = average_kinetics([r.kinetics for r in L]) else: - boo = False - - kinetics = kinetics.to_arrhenius(rxn.get_enthalpy_of_reaction(T)) + kinetics = kinetics.to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(298.)) + k = kinetics.get_rate_coefficient(T) errors[rxn] = np.log(k / krxn) - + else: + raise ValueError('only one piece of kinetics information in the tree?') + return errors, uncertainties def cross_validate_old(self, folds=5, T=1000.0, random_state=1, estimator='rate rules', thermo_database=None, get_reverse=False, uncertainties=True): @@ -3751,7 +3704,7 @@ def cross_validate_old(self, folds=5, T=1000.0, random_state=1, estimator='rate """ errors = {} uncs = {} - + kpu = KineticParameterUncertainty() rxns = np.array(self.get_training_set(remove_degeneracy=True,get_reverse=get_reverse)) @@ -3783,8 +3736,6 @@ def cross_validate_old(self, folds=5, T=1000.0, random_state=1, estimator='rate template = self.retrieve_template(template_labels) if estimator == 'rate rules': kinetics, entry = self.estimate_kinetics_using_rate_rules(template, degeneracy=1) - elif estimator == 'group additivity': - kinetics = self.estimate_kinetics_using_group_additivity(template, degeneracy=1) else: raise ValueError('{0} is not a valid value for input `estimator`'.format(estimator)) @@ -3797,7 +3748,7 @@ def cross_validate_old(self, folds=5, T=1000.0, random_state=1, estimator='rate boo,source = self.extract_source_from_comments(testrxn) sdict = {"Rate Rules":source} uncs[rxn] = kpu.get_uncertainty_value(sdict) - + if uncertainties: return errors, uncs else: @@ -3807,19 +3758,19 @@ def simple_regularization(self, node, template_rxn_map, test=True): """ Simplest regularization algorithm All nodes are made as specific as their descendant reactions - Training reactions are assumed to not generalize + Training reactions are assumed to not generalize For example if an particular atom at a node is Oxygen for all of its descendent reactions a reaction where it is Sulfur will never hit that node - unless it is the top node even if the tree did not split on the identity + unless it is the top node even if the tree did not split on the identity of that atom - - The test option to this function determines whether or not the reactions - under a node match the extended group before adding an extension. - If the test fails the extension is skipped. - - In general test=True is needed if the cascade algorithm was used + + The test option to this function determines whether or not the reactions + under a node match the extended group before adding an extension. + If the test fails the extension is skipped. + + In general test=True is needed if the cascade algorithm was used to generate the tree and test=False is ok if the cascade algorithm - wasn't used. + wasn't used. """ for child in node.children: @@ -3936,7 +3887,7 @@ def regularize(self, regularization=simple_regularization, keep_root=True, therm if template_rxn_map is None: if rxns is None: template_rxn_map = self.get_reaction_matches(thermo_database=thermo_database, remove_degeneracy=True, - get_reverse=True, exact_matches_only=False, fix_labels=True) + get_reverse=True, exact_matches_only=False, fix_labels=True, rxns_with_kinetics_only=False) else: template_rxn_map = self.get_reaction_matches(rxns=rxns, thermo_database=thermo_database, remove_degeneracy=True, get_reverse=True, exact_matches_only=False, @@ -4025,7 +3976,7 @@ def clean_tree(self): def save_generated_tree(self, path=None): """ - clears the rules and saves the family to its + clears the rules and saves the family to its current location in database """ if path is None: @@ -4035,11 +3986,11 @@ def save_generated_tree(self, path=None): self.save(path) def get_training_set(self, thermo_database=None, remove_degeneracy=False, estimate_thermo=True, fix_labels=False, - get_reverse=False): + get_reverse=False, rxns_with_kinetics_only=False): """ retrieves all reactions in the training set, assigns thermo to the species objects reverses reactions as necessary so that all reactions are in the forward direction - and returns the resulting list of reactions in the forward direction with thermo + and returns the resulting list of reactions in the forward direction with thermo assigned """ @@ -4096,8 +4047,8 @@ def get_reactant_thermo(reactant,metal): logging.info('Must be because you turned off the training depository.') return - rxns = deepcopy([i.item for i in dep.entries.values()]) - entries = deepcopy([i for i in dep.entries.values()]) + rxns = deepcopy([i.item for i in dep.entries.values() if (not rxns_with_kinetics_only) or type(i.data) != KineticsModel]) + entries = deepcopy([i for i in dep.entries.values() if (not rxns_with_kinetics_only) or type(i.data) != KineticsModel]) roots = [x.item for x in self.get_root_template()] root = None @@ -4109,7 +4060,7 @@ def get_reactant_thermo(reactant,metal): root_labels = [x.label for x in root.atoms if x.label != ''] root_label_set = set(root_labels) - + for i, entry in enumerate(entries): if estimate_thermo: # parse out the metal to scale to @@ -4127,7 +4078,7 @@ def get_reactant_thermo(reactant,metal): rxns[i].kinetics = entry.data rxns[i].rank = entry.rank - if remove_degeneracy: # adjust for degeneracy + if remove_degeneracy and type(rxns[i].kinetics) != KineticsModel: # adjust for degeneracy rxns[i].kinetics.A.value_si /= rxns[i].degeneracy mol = None @@ -4139,6 +4090,8 @@ def get_reactant_thermo(reactant,metal): else: mol = deepcopy(react.molecule[0]) + mol.update_atomtypes() + if fix_labels: for prod in rxns[i].products: fix_labels_mol(prod.molecule[0], root_labels) @@ -4157,12 +4110,12 @@ def get_reactant_thermo(reactant,metal): mol = mol.merge(react.molecule[0]) else: mol = deepcopy(react.molecule[0]) - + if fix_labels: mol_label_set = set([x.label for x in get_label_fixed_mol(mol, root_labels).atoms if x.label != '']) else: mol_label_set = set([x.label for x in mol.atoms if x.label != '']) - + if mol_label_set == root_label_set and ((mol.is_subgraph_isomorphic(root, generate_initial_map=True) or (not fix_labels and get_label_fixed_mol(mol, root_labels).is_subgraph_isomorphic(root, generate_initial_map=True)))): @@ -4195,7 +4148,10 @@ def get_reactant_thermo(reactant,metal): reacts = [Species(molecule=[get_label_fixed_mol(x.molecule[0], root_labels)], thermo=x.thermo) for x in rxns[i].reactants] - rrev = Reaction(reactants=products, products=reacts, + if type(rxns[i].kinetics) != KineticsModel: + if rxns[i].kinetics.solute: + rxns[i].kinetics.solute = to_soluteTSdata(rxns[i].kinetics.solute,reactants=rxns[i].reactants) + rrev = Reaction(reactants=products, products=reacts, kinetics=rxns[i].generate_reverse_rate_coefficient(), rank=rxns[i].rank) rrev.is_forward = False @@ -4224,6 +4180,8 @@ def get_reactant_thermo(reactant,metal): else: mol = deepcopy(react.molecule[0]) + mol.update_atomtypes() + if (mol.is_subgraph_isomorphic(root, generate_initial_map=True) or (not fix_labels and get_label_fixed_mol(mol, root_labels).is_subgraph_isomorphic(root, generate_initial_map=True))): # try product structures @@ -4250,15 +4208,15 @@ def get_reactant_thermo(reactant,metal): return rxns def get_reaction_matches(self, rxns=None, thermo_database=None, remove_degeneracy=False, estimate_thermo=True, - fix_labels=False, exact_matches_only=False, get_reverse=False): + fix_labels=False, exact_matches_only=False, get_reverse=False, rxns_with_kinetics_only=False): """ - returns a dictionary mapping for each entry in the tree: + returns a dictionary mapping for each entry in the tree: (entry.label,entry.item) : list of all training reactions (or the list given) that match that entry """ if rxns is None: rxns = self.get_training_set(thermo_database=thermo_database, remove_degeneracy=remove_degeneracy, estimate_thermo=estimate_thermo, fix_labels=fix_labels, - get_reverse=get_reverse) + get_reverse=get_reverse,rxns_with_kinetics_only=rxns_with_kinetics_only) entries = self.groups.entries @@ -4345,10 +4303,10 @@ def retrieve_original_entry(self, template_label): """ Retrieves the original entry, be it a rule or training reaction, given the template label in the form 'group1;group2' or 'group1;group2;group3' - + Returns tuple in the form (RateRuleEntry, TrainingReactionEntry) - + Where the TrainingReactionEntry is only present if it comes from a training reaction """ template_labels = template_label.split()[-1].split(';') @@ -4365,13 +4323,13 @@ def get_sources_for_template(self, template): """ Returns the set of rate rules and training reactions used to average this `template`. Note that the tree must be averaged with verbose=True for this to work. - + Returns a tuple of rules, training - - where rules are a list of tuples containing + + where rules are a list of tuples containing the [(original_entry, weight_used_in_average), ... ] - + and training is a list of tuples containing the [(rate_rule_entry, training_reaction_entry, weight_used_in_average),...] """ @@ -4379,7 +4337,7 @@ def get_sources_for_template(self, template): def assign_weights_to_entries(entry_nested_list, weighted_entries, n=1): """ Assign weights to an average of average nested list. Where n is the - number of values being averaged recursively. + number of values being averaged recursively. """ n = len(entry_nested_list) * n for entry in entry_nested_list: @@ -4453,7 +4411,7 @@ def assign_weights_to_entries(entry_nested_list, weighted_entries, n=1): rules[rule_entry] += weight else: rules[rule_entry] = weight - # Each entry should now only appear once + # Each entry should now only appear once training = [(k[0], k[1], v) for k, v in training.items()] rules = list(rules.items()) @@ -4464,11 +4422,11 @@ def extract_source_from_comments(self, reaction): Returns the rate rule associated with the kinetics of a reaction by parsing the comments. Will return the template associated with the matched rate rule. Returns a tuple containing (Boolean_Is_Kinetics_From_Training_reaction, Source_Data) - + For a training reaction, the Source_Data returns:: [Family_Label, Training_Reaction_Entry, Kinetics_In_Reverse?] - + For a reaction from rate rules, the Source_Data is a tuple containing:: [Family_Label, {'template': originalTemplate, @@ -4503,7 +4461,7 @@ def extract_source_from_comments(self, reaction): 'but does not match the training reaction {1} from the ' '{2} family.'.format(reaction, training_reaction_index, self.label)) - # Sometimes the matched kinetics could be in the reverse direction..... + # Sometimes the matched kinetics could be in the reverse direction..... if reaction.is_isomorphic(training_entry.item, either_direction=False, save_order=self.save_order): reverse = False else: @@ -4517,7 +4475,7 @@ def extract_source_from_comments(self, reaction): elif line.startswith('Multiplied by'): degeneracy = float(line.split()[-1]) - # Extract the rate rule information + # Extract the rate rule information full_comment_string = reaction.kinetics.comment.replace('\n', ' ') # The rate rule string is right after the phrase 'for rate rule' @@ -4599,8 +4557,12 @@ def get_objective_function(kinetics1, kinetics2, obj=information_gain, T=1000.0) Error using mean: Err_1 + Err_2 Split: abs(N1-N2) """ - ks1 = np.array([np.log(k.get_rate_coefficient(T)) for k in kinetics1]) - ks2 = np.array([np.log(k.get_rate_coefficient(T)) for k in kinetics2]) + if not isinstance(kinetics1[0], Marcus): + ks1 = np.array([np.log(k.get_rate_coefficient(T)) for k in kinetics1]) + ks2 = np.array([np.log(k.get_rate_coefficient(T)) for k in kinetics2]) + else: + ks1 = np.array([k.get_lmbd_i(T) for k in kinetics1]) + ks2 = np.array([k.get_lmbd_i(T) for k in kinetics2]) N1 = len(ks1) return obj(ks1, ks2), N1 == 0 @@ -4608,29 +4570,115 @@ def get_objective_function(kinetics1, kinetics2, obj=information_gain, T=1000.0) def _make_rule(rr): """ - function for parallelization of rule and uncertainty calculation + Function for parallelization of rule and uncertainty calculation + + Input: rr - tuple of (recipe, rxns, Tref, fmax, label, ranks) + rxns and ranks are lists of equal length. + Output: kinetics object, with uncertainty and comment attached. + If Blowers-Masel fitting is successful it will be ArrheniusBM or ArrheniusChargeTransferBM, + else Arrhenius, SurfaceChargeTransfer, or ArrheniusChargeTransfer. + Errors in Ln(k) at each reaction are treated as samples from a weighted normal distribution weights are inverse variance weights based on estimates of the error in Ln(k) for each individual reaction """ recipe, rxns, Tref, fmax, label, ranks = rr - n = len(rxns) for i, rxn in enumerate(rxns): rxn.rank = ranks[i] rxns = np.array(rxns) - data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rxns])) + rs = np.array([r for r in rxns if type(r.kinetics) != KineticsModel]) + n = len(rs) + if n > 0 and isinstance(rs[0].kinetics, Marcus): + kin = average_kinetics([r.kinetics for r in rs]) + return kin + data_mean = np.mean(np.log([r.kinetics.get_rate_coefficient(Tref) for r in rs])) if n > 0: - kin = ArrheniusBM().fit_to_reactions(rxns, recipe=recipe) + if isinstance(rs[0].kinetics, Arrhenius): + arr = ArrheniusBM + else: + arr = ArrheniusChargeTransferBM + if n > 1: + kin = arr().fit_to_reactions(rs, recipe=recipe) + if n == 1 or kin.E0.value_si < 0.0: + kin = average_kinetics([r.kinetics for r in rs]) + #kin.comment = "Only one reaction or Arrhenius BM fit bad. Instead averaged from {} reactions.".format(n) + if n == 1: + kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + else: + dlnks = np.array([ + np.log( + average_kinetics([r.kinetics for r in rs[list(set(range(len(rs))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + else: + if n == 1: + kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + else: + if isinstance(rs[0].kinetics, Arrhenius): + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + else: + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(Tref)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + + #site solute parameters + site_datas = [get_site_solute_data(rxn) for rxn in rxns] + site_datas = [sdata for sdata in site_datas if sdata is not None] + if len(site_datas) > 0: + site_data = SoluteTSData() + for sdata in site_datas: + site_data += sdata + site_data = site_data * (1.0/len(site_datas)) + kin.solute = site_data + return kin + else: + return None + + if isinstance(rs[0].kinetics, Arrhenius): + arr = ArrheniusBM + else: + arr = ArrheniusChargeTransferBM + if n > 1: + kin = arr().fit_to_reactions(rs, recipe=recipe) + if n == 1 or kin.E0.value_si < 0.0: + # still run it through the averaging function when n=1 to standardize the units and run checks + kin = average_kinetics([r.kinetics for r in rs]) if n == 1: kin.uncertainty = RateUncertainty(mu=0.0, var=(np.log(fmax) / 2.0) ** 2, N=1, Tref=Tref, data_mean=data_mean, correlation=label) + kin.comment = f"Only one reaction rate: {rs[0]!s}" else: + kin.comment = f"Blowers-Masel fit was bad (E0<0) so instead averaged from {n} reactions." dlnks = np.array([ np.log( - ArrheniusBM().fit_to_reactions(rxns[list(set(range(len(rxns))) - {i})], recipe=recipe) - .to_arrhenius(rxn.get_enthalpy_of_reaction(Tref)) - .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) - ) for i, rxn in enumerate(rxns) - ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref - varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rxns]) / (2.0 * 8.314 * Tref)) ** 2 + average_kinetics([r.kinetics for r in rs[list(set(range(len(rs))) - {i})]]).get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 # weighted average calculations ws = 1.0 / varis V1 = ws.sum() @@ -4638,10 +4686,42 @@ def _make_rule(rr): mu = np.dot(ws, dlnks) / V1 s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) - return kin - else: - return None - + else: # Blowers-Masel fit was good + if isinstance(rs[0].kinetics, Arrhenius): + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius(rxn.get_enthalpy_of_reaction(298.)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + else: # SurfaceChargeTransfer or ArrheniusChargeTransfer + dlnks = np.array([ + np.log( + arr().fit_to_reactions(rs[list(set(range(len(rs))) - {i})], recipe=recipe) + .to_arrhenius_charge_transfer(rxn.get_enthalpy_of_reaction(298.)) + .get_rate_coefficient(T=Tref) / rxn.get_rate_coefficient(T=Tref) + ) for i, rxn in enumerate(rs) + ]) # 1) fit to set of reactions without the current reaction (k) 2) compute log(kfit/kactual) at Tref + varis = (np.array([rank_accuracy_map[rxn.rank].value_si for rxn in rs]) / (2.0 * 8.314 * Tref)) ** 2 + # weighted average calculations + ws = 1.0 / varis + V1 = ws.sum() + V2 = (ws ** 2).sum() + mu = np.dot(ws, dlnks) / V1 + s = np.sqrt(np.dot(ws, (dlnks - mu) ** 2) / (V1 - V2 / V1)) + kin.uncertainty = RateUncertainty(mu=mu, var=s ** 2, N=n, Tref=Tref, data_mean=data_mean, correlation=label) + + #site solute parameters + site_datas = [get_site_solute_data(rxn) for rxn in rxns] + site_datas = [sdata for sdata in site_datas if sdata is not None] + if len(site_datas) > 0: + site_data = SoluteTSData() + for sdata in site_datas: + site_data += sdata + site_data = site_data * (1.0/len(site_datas)) + kin.solute = site_data + return kin def _spawn_tree_process(family, template_rxn_map, obj, T, nprocs, depth, min_splitable_entry_num, min_rxns_to_spawn, extension_iter_max, extension_iter_item_cap): parent_conn, child_conn = mp.Pipe() @@ -4666,7 +4746,154 @@ def _child_make_tree_nodes(family, child_conn, template_rxn_map, obj, T, nprocs, family.groups.entries[root_label].parent = None family.make_tree_nodes(template_rxn_map=template_rxn_map, obj=obj, T=T, nprocs=nprocs, depth=depth + 1, - min_splitable_entry_num=min_splitable_entry_num, min_rxns_to_spawn=min_rxns_to_spawn, + min_splitable_entry_num=min_splitable_entry_num, min_rxns_to_spawn=min_rxns_to_spawn, extension_iter_max=extension_iter_max, extension_iter_item_cap=extension_iter_item_cap) child_conn.send(list(family.groups.entries.values())) + +def average_kinetics(kinetics_list): + """ + Based on averaging log k. + Hence we average n, Ea, arithmetically, but we + average log A (geometric average) + """ + if type(kinetics_list[0]) not in [Arrhenius,SurfaceChargeTransfer,ArrheniusChargeTransfer,Marcus]: + raise Exception('Invalid kinetics type {0!r} for {1!r}.'.format(type(kinetics), self)) + + Aunits = kinetics_list[0].A.units + if Aunits in {'cm^3/(mol*s)', 'cm^3/(molecule*s)', 'm^3/(molecule*s)'}: + Aunits = 'm^3/(mol*s)' + elif Aunits in {'cm^6/(mol^2*s)', 'cm^6/(molecule^2*s)', 'm^6/(molecule^2*s)'}: + Aunits = 'm^6/(mol^2*s)' + elif Aunits in {'s^-1', 'm^3/(mol*s)', 'm^6/(mol^2*s)'}: + # they were already in SI + pass + elif Aunits in {'m^2/(mol*s)', 'cm^2/(mol*s)', 'm^2/(molecule*s)', 'cm^2/(molecule*s)'}: + # surface: bimolecular (Langmuir-Hinshelwood) + Aunits = 'm^2/(mol*s)' + elif Aunits in {'m^5/(mol^2*s)', 'cm^5/(mol^2*s)', 'm^5/(molecule^2*s)', 'cm^5/(molecule^2*s)'}: + # surface: dissociative adsorption + Aunits = 'm^5/(mol^2*s)' + elif Aunits == '': + # surface: sticking coefficient + pass + else: + raise Exception('Invalid units {0} for averaging kinetics.'.format(Aunits)) + + logA = 0.0 + n = 0.0 + Ea = 0.0 + alpha = 0.5 + lmbd_i_coefs = np.zeros(4) + beta = 0.0 + wr = 0.0 + wp = 0.0 + electrons = None + if isinstance(kinetics_list[0], SurfaceChargeTransfer) or isinstance(kinetics_list[0], ArrheniusChargeTransfer): + if electrons is None: + electrons = kinetics_list[0].electrons.value_si + assert all(np.abs(k.V0.value_si) < 0.0001 for k in kinetics_list), [k.V0.value_si for k in kinetics_list] + assert all(np.abs(k.alpha.value_si - 0.5) < 0.001 for k in kinetics_list), [k.alpha for k in kinetics_list] + V0 = 0.0 + count = 0 + for kinetics in kinetics_list: + count += 1 + logA += np.log10(kinetics.A.value_si) + n += kinetics.n.value_si + if hasattr(kinetics,"Ea"): + Ea += kinetics.Ea.value_si + if hasattr(kinetics,"lmbd_i_coefs"): + lmbd_i_coefs += kinetics.lmbd_i_coefs.value_si + beta += kinetics.beta.value_si + wr += kinetics.wr.value_si + wp += kinetics.wp.value_si + + logA /= count + n /= count + Ea /= count + lmbd_i_coefs /= count + beta /= count + wr /= count + wp /= count + + if isinstance(kinetics, Marcus): + averaged_kinetics = Marcus( + A=(10 ** logA, Aunits), + n=n, + lmbd_i_coefs=lmbd_i_coefs, + beta=(beta,"1/m"), + wr=(wr * 0.001, "kJ/mol"), + wp=(wp * 0.001, "kJ/mol"), + comment="Averaged from {} reactions.".format(len(kinetics_list)), + ) + elif isinstance(kinetics, SurfaceChargeTransfer): + averaged_kinetics = SurfaceChargeTransfer( + A=(10 ** logA, Aunits), + n=n, + electrons=electrons, + alpha=alpha, + V0=(V0,'V'), + Ea=(Ea * 0.001, "kJ/mol"), + ) + elif isinstance(kinetics, ArrheniusChargeTransfer): + averaged_kinetics = ArrheniusChargeTransfer( + A=(10 ** logA, Aunits), + n=n, + electrons=electrons, + alpha=alpha, + V0=(V0,'V'), + Ea=(Ea * 0.001, "kJ/mol"), + ) + else: + averaged_kinetics = Arrhenius( + A=(10 ** logA, Aunits), + n=n, + Ea=(Ea * 0.001, "kJ/mol"), + comment=f"Averaged from {len(kinetics_list)} rate expressions.", + ) + return averaged_kinetics + +def get_site_solute_data(rxn): + """ + apply kinetic solvent correction in this case the parameters are dGTSsite instead of GTS + """ + from molecule.data.rmg import get_db + solvation_database = get_db('solvation') + ts_data = rxn.kinetics.solute + if ts_data: + site_data = to_soluteTSdata(ts_data,reactants=rxn.reactants) + + #compute x from gas phase + GR = 0.0 + GP = 0.0 + + for reactant in rxn.reactants: + try: + GR += reactant.thermo.get_free_energy(298.0) + except Exception: + logging.error("Problem with reactant {!r} in reaction {!s}".format(reactant, rxn)) + raise + for product in rxn.products: + try: + GP += product.thermo.get_free_energy(298.0) + except Exception: + logging.error("Problem with product {!r} in reaction {!s}".format(reactant, rxn)) + raise + + dGrxn = GP-GR + if dGrxn > 0: + x = 1.0 + else: + x = 0.0 + + for spc in rxn.reactants: + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data -= spc_solute_data*(1.0-x) + + for spc in rxn.products: + spc_solute_data = to_soluteTSdata(solvation_database.get_solute_data(spc.copy(deep=True))) + site_data -= spc_solute_data*x + + return site_data + else: + return None diff --git a/molecule/data/kinetics/groups.py b/molecule/data/kinetics/groups.py index 528deaa..fee0de5 100644 --- a/molecule/data/kinetics/groups.py +++ b/molecule/data/kinetics/groups.py @@ -83,7 +83,7 @@ def load_entry(self, index, label, group, kinetics, reference=None, referenceTyp Method for parsing entries in database files. Note that these argument names are retained for backward compatibility. - nodal_distance is the distance between a given entry and its parent specified by a float + nodalDistance is the distance between a given entry and its parent specified by a float """ if (group[0:3].upper() == 'OR{' or group[0:4].upper() == 'AND{' or @@ -200,42 +200,6 @@ def get_reaction_template(self, reaction): return template - def estimate_kinetics_using_group_additivity(self, template, reference_kinetics, degeneracy=1): - """ - Determine the appropriate kinetics for a reaction with the given - `template` using group additivity. - - Returns just the kinetics. - """ - warnings.warn("Group additivity is no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - # Start with the generic kinetics of the top-level nodes - # Make a copy so we don't modify the original - kinetics = deepcopy(reference_kinetics) - - # Now add in more specific corrections if possible - for node in template: - entry = node - comment_line = "Matched node " - while entry.data is None and entry not in self.top: - # Keep climbing tree until you find a (non-top) node with data. - comment_line += "{0} >> ".format(entry.label) - entry = entry.parent - if entry.data is not None and entry not in self.top: - kinetics = self._multiply_kinetics_data(kinetics, entry.data) - comment_line += "{0} ({1})".format(entry.label, entry.long_desc.split('\n')[0]) - elif entry in self.top: - comment_line += "{0} (Top node)".format(entry.label) - kinetics.comment += comment_line + '\n' - - # Also include reaction-path degeneracy - - kinetics.change_rate(degeneracy) - - kinetics.comment += "Multiplied by reaction path degeneracy {0}".format(degeneracy) - - return kinetics - def _multiply_kinetics_data(self, kinetics1, kinetics2): """ Multiply two kinetics objects `kinetics1` and `kinetics2` of the same @@ -329,303 +293,3 @@ class together, returning their product as a new kinetics object of else: kinetics.comment = kinetics1.comment + ' + ' + kinetics2.comment return kinetics - - def generate_group_additivity_values(self, training_set, kunits, method='Arrhenius'): - """ - Generate the group additivity values using the given `training_set`, - a list of 2-tuples of the form ``(template, kinetics)``. You must also - specify the `kunits` for the family and the `method` to use when - generating the group values. Returns ``True`` if the group values have - changed significantly since the last time they were fitted, or ``False`` - otherwise. - """ - warnings.warn("Group additivity is no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - # keep track of previous values so we can detect if they change - old_entries = dict() - for label, entry in self.entries.items(): - if entry.data is not None: - old_entries[label] = entry.data - - # Determine a complete list of the entries in the database, sorted as in the tree - group_entries = self.top[:] - for entry in self.top: - group_entries.extend(self.descendants(entry)) - - # Determine a unique list of the groups we will be able to fit parameters for - group_list = [] - for template, kinetics in training_set: - for group in template: - if group not in self.top: - group_list.append(group) - group_list.extend(self.ancestors(group)[:-1]) - group_list = list(set(group_list)) - group_list.sort(key=lambda x: x.index) - - if method == 'KineticsData': - # Fit a discrete set of k(T) data points by training against k(T) data - - Tdata = np.array([300, 400, 500, 600, 800, 1000, 1500, 2000]) - - # Initialize dictionaries of fitted group values and uncertainties - group_values = {} - group_uncertainties = {} - group_counts = {} - group_comments = {} - for entry in group_entries: - group_values[entry] = [] - group_uncertainties[entry] = [] - group_counts[entry] = [] - group_comments[entry] = set() - - # Generate least-squares matrix and vector - A = [] - b = [] - - kdata = [] - for template, kinetics in training_set: - - if isinstance(kinetics, (Arrhenius, KineticsData)): - kd = [kinetics.get_rate_coefficient(T) for T in Tdata] - elif isinstance(kinetics, ArrheniusEP): - kd = [kinetics.get_rate_coefficient(T, 0) for T in Tdata] - else: - raise TypeError('Unexpected kinetics model of type {0} for template ' - '{1}.'.format(kinetics.__class__, template)) - kdata.append(kd) - - # Create every combination of each group and its ancestors with each other - combinations = [] - for group in template: - groups = [group] - groups.extend(self.ancestors(group)) - combinations.append(groups) - combinations = get_all_combinations(combinations) - # Add a row to the matrix for each combination - for groups in combinations: - Arow = [1 if group in groups else 0 for group in group_list] - Arow.append(1) - brow = [math.log10(k) for k in kd] - A.append(Arow) - b.append(brow) - - for group in groups: - group_comments[group].add("{0!s}".format(template)) - - if len(A) == 0: - logging.warning('Unable to fit kinetics groups for family "{0}"; ' - 'no valid data found.'.format(self.label)) - return - A = np.array(A) - b = np.array(b) - kdata = np.array(kdata) - - x, residues, rank, s = np.linalg.lstsq(A, b, rcond=RCOND) - - for t, T in enumerate(Tdata): - - # Determine error in each group (on log scale) - stdev = np.zeros(len(group_list) + 1, np.float64) - count = np.zeros(len(group_list) + 1, np.int) - - for index in range(len(training_set)): - template, kinetics = training_set[index] - kd = math.log10(kdata[index, t]) - km = x[-1, t] + sum([x[group_list.index(group), t] for group in template if group in group_list]) - variance = (km - kd) ** 2 - for group in template: - groups = [group] - groups.extend(self.ancestors(group)) - for g in groups: - if g not in self.top: - ind = group_list.index(g) - stdev[ind] += variance - count[ind] += 1 - stdev[-1] += variance - count[-1] += 1 - stdev = np.sqrt(stdev / (count - 1)) - import scipy.stats - ci = scipy.stats.t.ppf(0.975, count - 1) * stdev - - # Update dictionaries of fitted group values and uncertainties - for entry in group_entries: - if entry == self.top[0]: - group_values[entry].append(10 ** x[-1, t]) - group_uncertainties[entry].append(10 ** ci[-1]) - group_counts[entry].append(count[-1]) - elif entry in group_list: - index = group_list.index(entry) - group_values[entry].append(10 ** x[index, t]) - group_uncertainties[entry].append(10 ** ci[index]) - group_counts[entry].append(count[index]) - else: - group_values[entry] = None - group_uncertainties[entry] = None - group_counts[entry] = None - - # Store the fitted group values and uncertainties on the associated entries - for entry in group_entries: - if group_values[entry] is not None: - entry.data = KineticsData(Tdata=(Tdata, "K"), kdata=(group_values[entry], kunits)) - if not any(np.isnan(np.array(group_uncertainties[entry]))): - entry.data.kdata.uncertainties = np.array(group_uncertainties[entry]) - entry.data.kdata.uncertainty_type = '*|/' - entry.short_desc = "Group additive kinetics." - entry.long_desc = "Fitted to {0} rates.\n".format(group_counts[entry]) - entry.long_desc += "\n".join(group_comments[entry]) - else: - entry.data = None - - elif method == 'Arrhenius': - # Fit Arrhenius parameters (A, n, Ea) by training against k(T) data - - Tdata = np.array([300, 400, 500, 600, 800, 1000, 1500, 2000]) - logTdata = np.log(Tdata) - Tinvdata = 1000. / (constants.R * Tdata) - - A = [] - b = [] - - kdata = [] - for template, kinetics in training_set: - - if isinstance(kinetics, (Arrhenius, KineticsData)): - kd = [kinetics.get_rate_coefficient(T) for T in Tdata] - elif isinstance(kinetics, ArrheniusEP): - kd = [kinetics.get_rate_coefficient(T, 0) for T in Tdata] - else: - raise TypeError('Unexpected kinetics model of type {0} for template ' - '{1}.'.format(kinetics.__class__, template)) - kdata.append(kd) - - # Create every combination of each group and its ancestors with each other - combinations = [] - for group in template: - groups = [group] - groups.extend(self.ancestors(group)) - combinations.append(groups) - combinations = get_all_combinations(combinations) - - # Add a row to the matrix for each combination at each temperature - for t, T in enumerate(Tdata): - logT = logTdata[t] - Tinv = Tinvdata[t] - for groups in combinations: - Arow = [] - for group in group_list: - if group in groups: - Arow.extend([1, logT, -Tinv]) - else: - Arow.extend([0, 0, 0]) - Arow.extend([1, logT, -Tinv]) - brow = math.log(kd[t]) - A.append(Arow) - b.append(brow) - - if len(A) == 0: - logging.warning('Unable to fit kinetics groups for family "{0}"; ' - 'no valid data found.'.format(self.label)) - return - A = np.array(A) - b = np.array(b) - kdata = np.array(kdata) - - x, residues, rank, s = np.linalg.lstsq(A, b, rcond=RCOND) - - # Store the results - self.top[0].data = Arrhenius( - A=(math.exp(x[-3]), kunits), - n=x[-2], - Ea=(x[-1], "kJ/mol"), - T0=(1, "K"), - ) - for i, group in enumerate(group_list): - group.data = Arrhenius( - A=(math.exp(x[3 * i]), kunits), - n=x[3 * i + 1], - Ea=(x[3 * i + 2], "kJ/mol"), - T0=(1, "K"), - ) - - elif method == 'Arrhenius2': - # Fit Arrhenius parameters (A, n, Ea) by training against (A, n, Ea) values - - A = [] - b = [] - - for template, kinetics in training_set: - - # Create every combination of each group and its ancestors with each other - combinations = [] - for group in template: - groups = [group] - groups.extend(self.ancestors(group)) - combinations.append(groups) - combinations = get_all_combinations(combinations) - - # Add a row to the matrix for each parameter - if (isinstance(kinetics, Arrhenius) or - (isinstance(kinetics, ArrheniusEP) and kinetics.alpha.value_si == 0)): - for groups in combinations: - Arow = [] - for group in group_list: - if group in groups: - Arow.append(1) - else: - Arow.append(0) - Arow.append(1) - Ea = kinetics.E0.value_si if isinstance(kinetics, ArrheniusEP) else kinetics.Ea.value_si - brow = [math.log(kinetics.A.value_si), kinetics.n.value_si, Ea / 1000.] - A.append(Arow) - b.append(brow) - - if len(A) == 0: - logging.warning('Unable to fit kinetics groups for family "{0}"; ' - 'no valid data found.'.format(self.label)) - return - A = np.array(A) - b = np.array(b) - - x, residues, rank, s = np.linalg.lstsq(A, b, rcond=RCOND) - - # Store the results - self.top[0].data = Arrhenius( - A=(math.exp(x[-1, 0]), kunits), - n=x[-1, 1], - Ea=(x[-1, 2], "kJ/mol"), - T0=(1, "K"), - ) - for i, group in enumerate(group_list): - group.data = Arrhenius( - A=(math.exp(x[i, 0]), kunits), - n=x[i, 1], - Ea=(x[i, 2], "kJ/mol"), - T0=(1, "K"), - ) - - # Add a note to the history of each changed item indicating that we've generated new group values - changed = False - for label, entry in self.entries.items(): - if entry.data is not None and label in old_entries: - if (isinstance(entry.data, KineticsData) and - isinstance(old_entries[label], KineticsData) and - len(entry.data.kdata.value_si) == len(old_entries[label].kdata.value_si) and - all(abs(entry.data.kdata.value_si / old_entries[label].kdata.value_si - 1) < 0.01)): - # New group values within 1% of old - pass - elif (isinstance(entry.data, Arrhenius) and - isinstance(old_entries[label], Arrhenius) and - abs(entry.data.A.value_si / old_entries[label].A.value_si - 1) < 0.01 and - abs(entry.data.n.value_si / old_entries[label].n.value_si - 1) < 0.01 and - abs(entry.data.Ea.value_si / old_entries[label].Ea.value_si - 1) < 0.01 and - abs(entry.data.T0.value_si / old_entries[label].T0.value_si - 1) < 0.01): - # New group values within 1% of old - pass - else: - changed = True - break - else: - changed = True - break - - return changed diff --git a/molecule/data/kinetics/library.py b/molecule/data/kinetics/library.py index 5fb2949..3e297ec 100644 --- a/molecule/data/kinetics/library.py +++ b/molecule/data/kinetics/library.py @@ -43,11 +43,13 @@ from molecule.data.kinetics.common import save_entry from molecule.data.kinetics.family import TemplateReaction from molecule.kinetics import Arrhenius, ThirdBody, Lindemann, Troe, \ - PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, Chebyshev + PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, Chebyshev, KineticsModel, Marcus +from molecule.kinetics.surface import StickingCoefficient from molecule.molecule import Molecule from molecule.reaction import Reaction from molecule.species import Species - +from molecule.data.solvation import to_soluteTSdata +import molecule.constants as constants ################################################################################ @@ -208,6 +210,62 @@ def generate_high_p_limit_kinetics(self): " kinetics at P >= 100 bar.\n".format(self)) return False + def get_sticking_coefficient(self, T): + """ + Helper function to get sticking coefficient + """ + if isinstance(self.kinetics, StickingCoefficient): + stick = self.kinetics.get_sticking_coefficient(T) + return stick + + return False + + def apply_solvent_correction(self, solvent): + """ + apply kinetic solvent correction + """ + from molecule.data.rmg import get_db + solvation_database = get_db('solvation') + solvent_data = solvation_database.get_solvent_data(solvent) + + if isinstance(self.kinetics, Marcus): + solvent_struct = solvation_database.get_solvent_structure(solvent) + solv_solute_data = solvation_database.get_solute_data(solvent_struct.copy(deep=True)) + Rsolv = math.pow((75 * solv_solute_data.V / constants.pi / constants.Na) * (1.0 / 3.0)) / 100 + Rtot = 0.0 + Ner = 0 + Nep = 0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc.copy(deep=True)) + spc_solute_data.set_mcgowan_volume(spc) + R = math.pow((75 * spc_solute_data.V / constants.pi / constants.Na), + (1.0 / 3.0)) / 100 + Rtot += R + Ner += spc.get_net_charge() + for spc in self.products: + Nep += spc.get_net_charge() + + Rtot += Rsolv #radius of reactants plus first solvation shell + self.lmbd_o = constants.Na*(constants.e*(Nep-Ner))**2/(8.0*constants.pi*constants.epsilon_0*Rtot)*(1.0/solvent_data.n**2 - 1.0/solvent_data.eps) + return + + solute_data = to_soluteTSdata(self.kinetics.solute,reactants=self.reactants) + dGTS,dHTS = solute_data.calculate_corrections(solvent_data) + dSTS = (dHTS - dGTS)/298.0 + + dHR = 0.0 + dSR = 0.0 + for spc in self.reactants: + spc_solute_data = solvation_database.get_solute_data(spc) + spc_correction = solvation_database.get_solvation_correction(spc_solute_data, solvent_data) + dHR += spc_correction.enthalpy + dSR += spc_correction.entropy + + dH = dHTS-dHR + dA = np.exp((dSTS-dSR)/constants.R) + self.kinetics.Ea.value_si += dH + self.kinetics.A.value_si *= dA + self.kinetics.comment += "solvation correction raised barrier by {0} kcal/mol and prefactor by factor of {1}".format(dH/4184.0,dA) ################################################################################ @@ -406,11 +464,16 @@ def load(self, path, local_context=None, global_context=None): local_context[key] = value # Process the file - f = open(path, 'r') + with open(path, 'r') as f: + content = f.read() try: - exec(f.read(), global_context, local_context) - except Exception: - logging.error('Error while reading database {0!r}.'.format(path)) + exec(content, global_context, local_context) + except Exception as e: + logging.exception(f'Error while reading database file {path}.') + line_number = e.__traceback__.tb_next.tb_lineno + logging.error(f'Error occurred at or near line {line_number} of {path}.') + lines = content.splitlines() + logging.error(f'Line: {lines[line_number - 1]}') raise f.close() diff --git a/molecule/data/kinetics/rules.py b/molecule/data/kinetics/rules.py index 276109b..ddf10a0 100644 --- a/molecule/data/kinetics/rules.py +++ b/molecule/data/kinetics/rules.py @@ -44,7 +44,8 @@ from molecule.data.base import Database, Entry, get_all_combinations from molecule.data.kinetics.common import save_entry from molecule.exceptions import KineticsError, DatabaseError -from molecule.kinetics import ArrheniusEP, Arrhenius, StickingCoefficientBEP, SurfaceArrheniusBEP +from molecule.kinetics import ArrheniusEP, Arrhenius, StickingCoefficientBEP, SurfaceArrheniusBEP, \ + SurfaceChargeTransfer, SurfaceChargeTransferBEP, Marcus from molecule.quantity import Quantity, ScalarQuantity from molecule.reaction import Reaction @@ -109,289 +110,6 @@ def save_entry(self, f, entry): """ return save_entry(f, entry) - def process_old_library_entry(self, data): - """ - Process a list of parameters `data` as read from an old-style RMG - thermo database, returning the corresponding kinetics object. - """ - warnings.warn("The old kinetics databases are no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - # The names of all of the RMG reaction families that are bimolecular - BIMOLECULAR_KINETICS_FAMILIES = [ - 'H_Abstraction', - 'R_Addition_MultipleBond', - 'R_Recombination', - 'Disproportionation', - '1+2_Cycloaddition', - '2+2_cycloaddition_Cd', - '2+2_cycloaddition_CO', - '2+2_cycloaddition_CCO', - 'Diels_alder_addition', - '1,2_Insertion', - '1,3_Insertion_CO2', - '1,3_Insertion_ROR', - 'R_Addition_COm', - 'Oa_R_Recombination', - 'Substitution_O', - 'SubstitutionS', - 'R_Addition_CSm', - '1,3_Insertion_RSR', - 'lone_electron_pair_bond', - ] - - # The names of all of the RMG reaction families that are unimolecular - UNIMOLECULAR_KINETICS_FAMILIES = [ - 'intra_H_migration', - 'Birad_recombination', - 'intra_OH_migration', - 'HO2_Elimination_from_PeroxyRadical', - 'H_shift_cyclopentadiene', - 'Cyclic_Ether_Formation', - 'Intra_R_Add_Exocyclic', - 'Intra_R_Add_Endocyclic', - '1,2-Birad_to_alkene', - 'Intra_Disproportionation', - 'Korcek_step1', - 'Korcek_step2', - '1,2_shiftS', - 'intra_substitutionCS_cyclization', - 'intra_substitutionCS_isomerization', - 'intra_substitutionS_cyclization', - 'intra_substitutionS_isomerization', - 'intra_NO2_ONO_conversion', - '1,4_Cyclic_birad_scission', - '1,4_Linear_birad_scission', - 'Intra_Diels_alder', - 'ketoenol', - 'Retroen' - ] - # This is hardcoding of reaction families! - label = os.path.split(self.label)[-2] - if label in BIMOLECULAR_KINETICS_FAMILIES: - Aunits = 'cm^3/(mol*s)' - elif label in UNIMOLECULAR_KINETICS_FAMILIES: - Aunits = 's^-1' - else: - raise Exception('Unable to determine preexponential units for old reaction family ' - '"{0}".'.format(self.label)) - - try: - Tmin, Tmax = data[0].split('-') - Tmin = (float(Tmin), "K") - Tmax = (float(Tmax), "K") - except ValueError: - Tmin = (float(data[0]), "K") - Tmax = None - - A, n, alpha, E0, dA, dn, dalpha, dE0 = data[1:9] - - A = float(A) - if dA[0] == '*': - A = Quantity(A, Aunits, '*|/', float(dA[1:])) - else: - dA = float(dA) - if dA: - A = Quantity(A, Aunits, '+|-', dA) - else: - A = Quantity(A, Aunits) - - n = float(n) - dn = float(dn) - if dn: - n = Quantity(n, '', '+|-', dn) - else: - n = Quantity(n, '') - - alpha = float(alpha) - dalpha = float(dalpha) - if dalpha: - alpha = Quantity(alpha, '', '+|-', dalpha) - else: - alpha = Quantity(alpha, '') - - E0 = float(E0) - dE0 = float(dE0) - if dE0: - E0 = Quantity(E0, 'kcal/mol', '+|-', dE0) - else: - E0 = Quantity(E0, 'kcal/mol') - - rank = int(data[9]) - - return ArrheniusEP(A=A, n=n, alpha=alpha, E0=E0, Tmin=Tmin, Tmax=Tmax), rank - - def load_old(self, path, groups, num_labels): - """ - Load a set of old rate rules for kinetics groups into this depository. - """ - warnings.warn("The old kinetics databases are no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - # Parse the old library - entries = self.parse_old_library(os.path.join(path, 'rateLibrary.txt'), num_parameters=10, num_labels=num_labels) - - self.entries = {} - for entry in entries: - index, label, data, shortDesc = entry - if isinstance(data, str): - kinetics = data - rank = 0 - elif isinstance(data, tuple) and len(data) == 2: - kinetics, rank = data - else: - raise DatabaseError('Unexpected data {0!r} for entry {1!s}.'.format(data, entry)) - reactants = [groups.entries[l].item for l in label.split(';')] - item = Reaction(reactants=reactants, products=[]) - entry = Entry( - index=index, - label=label, - item=item, - data=kinetics, - rank=rank, - short_desc=shortDesc - ) - try: - self.entries[label].append(entry) - except KeyError: - self.entries[label] = [entry] - self._load_old_comments(path) - - def _load_old_comments(self, path): - """ - Load a set of old comments from the ``comments.txt`` file for the old - kinetics groups. This function assumes that the groups have already - been loaded. - """ - warnings.warn("The old kinetics databases are no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - index = 'General' # mops up comments before the first rate ID - - re_underline = re.compile(r'^\-+') - - comments = {} - comments[index] = '' - - # Load the comments into a temporary dictionary for now - # If no comments file then do nothing - try: - f = codecs.open(os.path.join(path, 'comments.rst'), 'r', 'utf-8') - except IOError: - return - for line in f: - match = re_underline.match(line) - if match: - index = f.next().strip() - assert line.rstrip() == f.next().rstrip(), "Overline didn't match underline" - if index not in comments: - comments[index] = '' - line = next(f) - comments[index] += line - f.close() - - # Transfer the comments to the long_desc attribute of the associated entry - entries = self.get_entries() - unused = [] - for index, longDesc in comments.items(): - try: - index = int(index) - except ValueError: - unused.append(index) - - if isinstance(index, int): - for entry in entries: - if entry.index == index: - entry.long_desc = longDesc - break - # else: - # unused.append(str(index)) - - # Any unused comments are placed in the long_desc attribute of the depository - self.long_desc = comments['General'] + '\n' - unused.remove('General') - for index in unused: - try: - self.long_desc += comments[index] + '\n' - except KeyError: - import pdb - pdb.set_trace() - - def save_old(self, path, groups): - """ - Save a set of old rate rules for kinetics groups from this depository. - """ - warnings.warn("The old kinetics databases are no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - # This is hardcoding of reaction families! - label = os.path.split(self.label)[-2] - reaction_order = groups.groups.reactant_num - if reaction_order == 2: - factor = 1.0e6 - elif reaction_order == 1: - factor = 1.0 - else: - raise ValueError('Unable to determine preexponential units for old reaction family ' - '"{0}".'.format(self.label)) - - entries = self.get_entries() - - flib = codecs.open(os.path.join(path, 'rateLibrary.txt'), 'w', 'utf-8') - flib.write('// The format for the data in this rate library\n') - flib.write('Arrhenius_EP\n\n') - - fcom = codecs.open(os.path.join(path, 'comments.rst'), 'w', 'utf-8') - fcom.write('-------\n') - fcom.write('General\n') - fcom.write('-------\n') - fcom.write(self.long_desc.strip() + '\n\n') - - for entry in entries: - flib.write('{0:<5d} '.format(entry.index)) - line = '' - for label in entry.label.split(';'): - line = line + '{0:<23} '.format(label) - flib.write(line) - if len(line) > 48: # make long lines line up in 10-space columns - flib.write(' ' * (10 - len(line) % 10)) - if entry.data.Tmax is None: - if re.match(r'\d+\-\d+', str(entry.data.Tmin).strip()): - # Tmin contains string of Trange - Trange = '{0} '.format(entry.data.Tmin) - elif isinstance(entry.data.Tmin, ScalarQuantity): - # Tmin is a temperature. Make range 1 degree either side! - Trange = '{0:4g}-{1:g} '.format(entry.data.Tmin.value_si - 1, entry.data.Tmin.value_si + 1) - else: - # Range is missing, but we have to put something: - Trange = ' 1-9999 ' - else: - Trange = '{0:4g}-{1:g} '.format(entry.data.Tmin.value_si, entry.data.Tmax.value_si) - flib.write('{0:<12}'.format(Trange)) - flib.write('{0:11.2e} {1:9.2f} {2:9.2f} {3:11.2f} '.format( - entry.data.A.value_si * factor, - entry.data.n.value_si, - entry.data.alpha.value_si, - entry.data.E0.value_si / 4184. - )) - if entry.data.A.is_uncertainty_multiplicative(): - flib.write('*{0:<6g} '.format(entry.data.A.uncertainty_si)) - else: - flib.write('{0:<7g} '.format(entry.data.A.uncertainty_si * factor)) - flib.write('{0:6g} {1:6g} {2:6g} '.format( - entry.data.n.uncertainty_si, - entry.data.alpha.uncertainty_si, - entry.data.E0.uncertainty_si / 4184. - )) - - if not entry.rank: - entry.rank = 0 - flib.write(u' {0:<4d} {1}\n'.format(entry.rank, entry.short_desc)) - - fcom.write('------\n') - fcom.write('{0}\n'.format(entry.index)) - fcom.write('------\n') - fcom.write(entry.long_desc.strip() + '\n\n') - - flib.close() - fcom.close() - def get_entries(self): """ Return a list of all of the entries in the rate rules database, @@ -565,12 +283,23 @@ def _get_average_kinetics(self, kinetics_list): n = 0.0 E0 = 0.0 alpha = 0.0 - count = len(kinetics_list) + electrons = None + V0 = None + count = 0 for kinetics in kinetics_list: + if isinstance(kinetics, SurfaceChargeTransfer): + continue + count += 1 logA += math.log10(kinetics.A.value_si) n += kinetics.n.value_si alpha += kinetics.alpha.value_si E0 += kinetics.E0.value_si + if isinstance(kinetics, SurfaceChargeTransferBEP): + if electrons is None: + electrons = kinetics.electrons.value_si + if V0 is None: + V0 = kinetics.V0.value_si + logA /= count n /= count alpha /= count @@ -595,15 +324,25 @@ def _get_average_kinetics(self, kinetics_list): else: raise Exception('Invalid units {0} for averaging kinetics.'.format(Aunits)) - if type(kinetics) not in [ArrheniusEP, SurfaceArrheniusBEP, StickingCoefficientBEP]: + if type(kinetics) not in [ArrheniusEP, SurfaceArrheniusBEP, StickingCoefficientBEP, SurfaceChargeTransferBEP]: raise Exception('Invalid kinetics type {0!r} for {1!r}.'.format(type(kinetics), self)) - averaged_kinetics = type(kinetics)( - A=(10 ** logA, Aunits), - n=n, - alpha=alpha, - E0=(E0 * 0.001, "kJ/mol"), - ) + if isinstance(kinetics, SurfaceChargeTransferBEP): + averaged_kinetics = SurfaceChargeTransferBEP( + A=(10 ** logA, Aunits), + n=n, + electrons=electrons, + alpha=alpha, + V0=(V0,'V'), + E0=(E0 * 0.001, "kJ/mol"), + ) + else: + averaged_kinetics = type(kinetics)( + A=(10 ** logA, Aunits), + n=n, + alpha=alpha, + E0=(E0 * 0.001, "kJ/mol"), + ) return averaged_kinetics def estimate_kinetics(self, template, degeneracy=1): diff --git a/molecule/data/rmg.py b/molecule/data/rmg.py index 757f0e3..2a06e1f 100644 --- a/molecule/data/rmg.py +++ b/molecule/data/rmg.py @@ -80,6 +80,7 @@ def load(self, kinetics_families=None, kinetics_depositories=None, statmech_libraries=None, + adsorption_groups='adsorptionPt111', depository=True, solvation=True, surface=True, # on by default, because solvation is also on by default @@ -109,16 +110,17 @@ def load(self, self.load_solvation(os.path.join(path, 'solvation')) if surface: - self.load_thermo(os.path.join(path, 'thermo'), thermo_libraries, depository, surface) + self.load_thermo(os.path.join(path, 'thermo'), thermo_libraries, depository, surface, adsorption_groups) - def load_thermo(self, path, thermo_libraries=None, depository=True, surface=False): + def load_thermo(self, path, thermo_libraries=None, depository=True, surface=False, adsorption_groups='adsorptionPt111'): """ Load the RMG thermo database from the given `path` on disk, where `path` points to the top-level folder of the RMG thermo database. """ self.thermo = ThermoDatabase() + self.thermo.adsorption_groups = adsorption_groups self.thermo.load(path, thermo_libraries, depository, surface) def load_transport(self, path, transport_libraries=None): diff --git a/molecule/data/solvation.py b/molecule/data/solvation.py index aa1c2d2..b1b8d16 100644 --- a/molecule/data/solvation.py +++ b/molecule/data/solvation.py @@ -138,6 +138,7 @@ def save_entry(f, entry): f.write(' alpha = {0!r},\n'.format(entry.data.alpha)) f.write(' beta = {0!r},\n'.format(entry.data.beta)) f.write(' eps = {0!r},\n'.format(entry.data.eps)) + f.write(' n = {0!r},\n'.format(entry.data.n)) f.write(' name_in_coolprop = "{0!s}",\n'.format(entry.data.name_in_coolprop)) f.write(' ),\n') elif entry.data is None: @@ -338,6 +339,19 @@ def average_solute_data(solute_data_list): # except: # raise DatabaseError(f"Gas-phase saturation density is not available for {compound_name} at {temp} K.") # return rho_g +# +# +# def get_gas_saturation_pressure(compound_name, temp): +# """ +# Returns the saturation pressure of the solvent in Pa if the solvent is available in CoolProp (i.e. name_in_coolprop is +# not None); raises DatabaseError if the solvent is not available in CoolProp (i.e. name_in_coolprop is None). +# """ +# try: +# Psat_g = PropsSI('P', 'T', temp, 'Q', 0, compound_name) # saturated gas phase pressure in Pa +# except: +# raise DatabaseError(f"Gas-phase saturation pressure is not available for {compound_name} at {temp} K.") +# return Psat_g + ################################################################################ @@ -348,7 +362,7 @@ class SolventData(object): def __init__(self, s_h=None, b_h=None, e_h=None, l_h=None, a_h=None, c_h=None, s_g=None, b_g=None, e_g=None, l_g=None, a_g=None, c_g=None, A=None, B=None, - C=None, D=None, E=None, alpha=None, beta=None, eps=None, name_in_coolprop=None): + C=None, D=None, E=None, alpha=None, beta=None, eps=None, name_in_coolprop=None, n=None): self.s_h = s_h self.b_h = b_h self.e_h = e_h @@ -372,6 +386,8 @@ def __init__(self, s_h=None, b_h=None, e_h=None, l_h=None, a_h=None, self.beta = beta # This is the dielectric constant self.eps = eps + #This is the index of refraction + self.n = n # This corresponds to the solvent's name in CoolProp. CoolProp is an external package used for # fluid property calculation. If the solvent is not available in CoolProp, this is set to None self.name_in_coolprop = name_in_coolprop @@ -390,17 +406,6 @@ def get_solvent_viscosity(self, T): """ return math.exp(self.A + (self.B / T) + (self.C * math.log(T)) + (self.D * (T ** self.E))) - - # def get_solvent_saturation_pressure(self, T): - # """ - # Returns the saturation pressure of the solvent in Pa if the solvent is available in CoolProp (i.e. name_in_coolprop is - # not None); raises DatabaseError if the solvent is not available in CoolProp (i.e. name_in_coolprop is None). - # """ - # if self.name_in_coolprop is not None: - # return PropsSI('P', 'T', T, 'Q', 0, self.name_in_coolprop) - # else: - # raise DatabaseError("Saturation pressure is not available for the solvent whose `name_in_coolprop` is None") - # # def get_solvent_density(self, T): # """ # Returns the density of the solvent in Pa if the solvent is available in CoolProp (i.e. name_in_coolprop is @@ -530,6 +535,299 @@ def set_mcgowan_volume(self, species): self.V = Vtot / 100 # division by 100 to get units correct. +class SoluteTSData(object): + """ + Stores Abraham parameters to characterize a solute + """ + # Set class variable with McGowan volumes + mcgowan_volumes = { + 1: 8.71, 2: 6.75, 3: 22.23, + 6: 16.35, 7: 14.39, 8: 12.43, 9: 10.47, 10: 8.51, + 14: 26.83, 15: 24.87, 16: 22.91, 17: 20.95, 18: 18.99, + 35: 26.21, 53: 34.53, + } + + def __init__(self, Sg_g=0.0, Bg_g=0.0, Eg_g=0.0, Lg_g=0.0, Ag_g=0.0, Cg_g=0.0, Sh_g=0.0, Bh_g=0.0, Eh_g=0.0, Lh_g=0.0, Ah_g=0.0, Ch_g=0.0, + K_g=0.0, Sg_h=0.0, Bg_h=0.0, Eg_h=0.0, Lg_h=0.0, Ag_h=0.0, Cg_h=0.0, Sh_h=0.0, Bh_h=0.0, Eh_h=0.0, Lh_h=0.0, Ah_h=0.0, Ch_h=0.0, K_h=0.0, comment=None): + """ + Xi_j correction is associated with calculating j (Gibbs or enthalpy) using solvent parameters for i (abraharm=g, mintz=h) + """ + self.Sg_g = Sg_g + self.Bg_g = Bg_g + self.Eg_g = Eg_g + self.Lg_g = Lg_g + self.Ag_g = Ag_g + self.Cg_g = Cg_g + self.Sh_g = Sh_g + self.Bh_g = Bh_g + self.Eh_g = Eh_g + self.Lh_g = Lh_g + self.Ah_g = Ah_g + self.Ch_g = Ch_g + self.K_g = K_g + + self.Sg_h = Sg_h + self.Bg_h = Bg_h + self.Eg_h = Eg_h + self.Lg_h = Lg_h + self.Ag_h = Ag_h + self.Cg_h = Cg_h + self.Sh_h = Sh_h + self.Bh_h = Bh_h + self.Eh_h = Eh_h + self.Lh_h = Lh_h + self.Ah_h = Ah_h + self.Ch_h = Ch_h + self.K_h = K_h + + self.comment = comment + + def __repr__(self): + return "SoluteTSData(Sg_g={0},Bg_g={1},Eg_g={2},Lg_g={3},Ag_g={4},Cg_g={5},Sh_g={6},Bh_g={7},Eh_g={8},Lh_g={9},Ah_g={10},Ch_g={11},K_g={12},Sg_h={13},Bg_h={14},Eg_h={15},Lg_h={16},Ag_h={17},Cg_h={18},Sh_h={19},Bh_h={20},Eh_h={21},Lh_h={22},Ah_h={23},Ch_h={24},K_h={25},comment={26!r})".format( + self.Sg_g, + self.Bg_g, + self.Eg_g, + self.Lg_g, + self.Ag_g, + self.Cg_g, + self.Sh_g, + self.Bh_g, + self.Eh_g, + self.Lh_g, + self.Ah_g, + self.Ch_g, + self.K_g, + self.Sg_h, + self.Bg_h, + self.Eg_h, + self.Lg_h, + self.Ag_h, + self.Cg_h, + self.Sh_h, + self.Bh_h, + self.Eh_h, + self.Lh_h, + self.Ah_h, + self.Ch_h, + self.K_h, self.comment) + + def __add__(self,sol): + return SoluteTSData( + Sg_g = self.Sg_g+sol.Sg_g, + Bg_g = self.Bg_g+sol.Bg_g, + Eg_g = self.Eg_g+sol.Eg_g, + Lg_g = self.Lg_g+sol.Lg_g, + Ag_g = self.Ag_g+sol.Ag_g, + Cg_g = self.Cg_g+sol.Cg_g, + Sh_g = self.Sh_g+sol.Sh_g, + Bh_g = self.Bh_g+sol.Bh_g, + Eh_g = self.Eh_g+sol.Eh_g, + Lh_g = self.Lh_g+sol.Lh_g, + Ah_g = self.Ah_g+sol.Ah_g, + Ch_g = self.Ch_g+sol.Ch_g, + K_g = self.K_g+sol.K_g, + Sg_h = self.Sg_h+sol.Sg_h, + Bg_h = self.Bg_h+sol.Bg_h, + Eg_h = self.Eg_h+sol.Eg_h, + Lg_h = self.Lg_h+sol.Lg_h, + Ag_h = self.Ag_h+sol.Ag_h, + Cg_h = self.Cg_h+sol.Cg_h, + Sh_h = self.Sh_h+sol.Sh_h, + Bh_h = self.Bh_h+sol.Bh_h, + Eh_h = self.Eh_h+sol.Eh_h, + Lh_h = self.Lh_h+sol.Lh_h, + Ah_h = self.Ah_h+sol.Ah_h, + Ch_h = self.Ch_h+sol.Ch_h, + K_h = self.K_h+sol.K_h, + ) + + def __sub__(self,sol): + return SoluteTSData( + Sg_g = self.Sg_g-sol.Sg_g, + Bg_g = self.Bg_g-sol.Bg_g, + Eg_g = self.Eg_g-sol.Eg_g, + Lg_g = self.Lg_g-sol.Lg_g, + Ag_g = self.Ag_g-sol.Ag_g, + Cg_g = self.Cg_g-sol.Cg_g, + Sh_g = self.Sh_g-sol.Sh_g, + Bh_g = self.Bh_g-sol.Bh_g, + Eh_g = self.Eh_g-sol.Eh_g, + Lh_g = self.Lh_g-sol.Lh_g, + Ah_g = self.Ah_g-sol.Ah_g, + Ch_g = self.Ch_g-sol.Ch_g, + K_g = self.K_g-sol.K_g, + Sg_h = self.Sg_h-sol.Sg_h, + Bg_h = self.Bg_h-sol.Bg_h, + Eg_h = self.Eg_h-sol.Eg_h, + Lg_h = self.Lg_h-sol.Lg_h, + Ag_h = self.Ag_h-sol.Ag_h, + Cg_h = self.Cg_h-sol.Cg_h, + Sh_h = self.Sh_h-sol.Sh_h, + Bh_h = self.Bh_h-sol.Bh_h, + Eh_h = self.Eh_h-sol.Eh_h, + Lh_h = self.Lh_h-sol.Lh_h, + Ah_h = self.Ah_h-sol.Ah_h, + Ch_h = self.Ch_h-sol.Ch_h, + K_h = self.K_h-sol.K_h, + ) + + def __eq__(self,sol): + if self.Sg_g != sol.Sg_g: + return False + elif self.Bg_g != sol.Bg_g: + return False + elif self.Eg_g != sol.Eg_g: + return False + elif self.Lg_g != sol.Lg_g: + return False + elif self.Ag_g != sol.Ag_g: + return False + elif self.Cg_g != sol.Cg_g: + return False + elif self.Sh_g != sol.Sh_g: + return False + elif self.Bh_g != sol.Bh_g: + return False + elif self.Eh_g != sol.Eh_g: + return False + elif self.Lh_g != sol.Lh_g: + return False + elif self.Ah_g != sol.Ah_g: + return False + elif self.Ch_g != sol.Ch_g: + return False + elif self.K_g != sol.K_g: + return False + elif self.Sg_h != sol.Sg_h: + return False + elif self.Bg_h != sol.Bg_h: + return False + elif self.Eg_h != sol.Eg_h: + return False + elif self.Lg_h != sol.Lg_h: + return False + elif self.Ag_h != sol.Ag_h: + return False + elif self.Cg_h != sol.Cg_h: + return False + elif self.Sh_h != sol.Sh_h: + return False + elif self.Bh_h != sol.Bh_h: + return False + elif self.Eh_h != sol.Eh_h: + return False + elif self.Lh_h != sol.Lh_h: + return False + elif self.Ah_h != sol.Ah_h: + return False + elif self.Ch_h != sol.Ch_h: + return False + elif self.K_h != sol.K_h: + return False + else: + return True + + def __mul__(self,num): + return SoluteTSData( + Sg_g = self.Sg_g*num, + Bg_g = self.Bg_g*num, + Eg_g = self.Eg_g*num, + Lg_g = self.Lg_g*num, + Ag_g = self.Ag_g*num, + Cg_g = self.Cg_g*num, + Sh_g = self.Sh_g*num, + Bh_g = self.Bh_g*num, + Eh_g = self.Eh_g*num, + Lh_g = self.Lh_g*num, + Ah_g = self.Ah_g*num, + Ch_g = self.Ch_g*num, + K_g = self.K_g*num, + Sg_h = self.Sg_h*num, + Bg_h = self.Bg_h*num, + Eg_h = self.Eg_h*num, + Lg_h = self.Lg_h*num, + Ag_h = self.Ag_h*num, + Cg_h = self.Cg_h*num, + Sh_h = self.Sh_h*num, + Bh_h = self.Bh_h*num, + Eh_h = self.Eh_h*num, + Lh_h = self.Lh_h*num, + Ah_h = self.Ah_h*num, + Ch_h = self.Ch_h*num, + K_h = self.K_h*num, + ) + + def calculate_corrections(self,solv): + dG298 = 0.0 + dG298 += -(np.log(10)*8.314*298.15)*(self.Sg_g*solv.s_g+self.Bg_g*solv.b_g+self.Eg_g*solv.e_g+self.Lg_g*solv.l_g+self.Ag_g*solv.a_g+self.Cg_g*solv.c_g+self.K_g) + dG298 += 1000.0*(self.Sh_g*solv.s_h+self.Bh_g*solv.b_h+self.Eh_g*solv.e_h+self.Lh_g*solv.l_h+self.Ah_g*solv.a_h+self.Ch_g*solv.c_h) + dH298 = 0.0 + dH298 += -(np.log(10)*8.314*298.15)*(self.Sg_h*solv.s_g+self.Bg_h*solv.b_g+self.Eg_h*solv.e_g+self.Lg_h*solv.l_g+self.Ag_h*solv.a_g+self.Cg_h*solv.c_g+self.K_h) + dH298 += 1000.0*(self.Sh_h*solv.s_h+self.Bh_h*solv.b_h+self.Eh_h*solv.e_h+self.Lh_h*solv.l_h+self.Ah_h*solv.a_h+self.Ch_h*solv.c_h) + return dG298,dH298 + +class SoluteTSDiffData(object): + """ + Stores Abraham parameters to characterize a solute + """ + # Set class variable with McGowan volumes + mcgowan_volumes = { + 1: 8.71, 2: 6.75, 3: 22.23, + 6: 16.35, 7: 14.39, 8: 12.43, 9: 10.47, 10: 8.51, + 14: 26.83, 15: 24.87, 16: 22.91, 17: 20.95, 18: 18.99, + 35: 26.21, 53: 34.53, + } + + def __init__(self, S_g=None, B_g=None, E_g=None, L_g=None, A_g=None, + K_g=None, S_h=None, B_h=None, E_h=None, L_h=None, A_h=None, K_h=None, comment=None): + self.S_g = S_g + self.B_g = B_g + self.E_g = E_g + self.L_g = L_g + self.A_g = A_g + self.K_g = K_g + self.S_h = S_h + self.B_h = B_h + self.E_h = E_h + self.L_h = L_h + self.A_h = A_h + self.K_h = K_h + + self.comment=comment + + def __repr__(self): + return "SoluteTSDiffData(S_g={0},B_g={1},E_g={2},L_g={3},A_g={4},K_g={5},S_h={6},B_h={7},E_h={8},L_h={9},A_h={10},K_h={11},comment={12!r})".format( + self.S_g, self.B_g, self.E_g, self.L_g, self.A_g, self.K_g, self.S_h, self.B_h, self.E_h, + self.L_h, self.A_h, self.K_h, self.comment) + +def to_soluteTSdata(data,reactants=None): + if isinstance(data,SoluteTSData): + return data + elif isinstance(data,SoluteData): + return SoluteTSData(Sg_g=data.S,Bg_g=data.B,Eg_g=data.E,Lg_g=data.L,Ag_g=data.A,Cg_g=1.0, + Sh_h=data.S,Bh_h=data.B,Eh_h=data.E,Lh_h=data.L,Ah_h=data.A,Ch_h=1.0,comment=data.comment) + elif isinstance(data,SoluteTSDiffData): + from molecule.data.rmg import get_db + solvation_database = get_db('solvation') + react_data = [solvation_database.get_solute_data(spc.copy(deep=True)) for spc in reactants] + return SoluteTSData(Sg_g=data.S_g+sum([x.S for x in react_data]), + Bg_g=data.B_g+sum([x.B for x in react_data]), + Eg_g=data.E_g+sum([x.E for x in react_data]), + Lg_g=data.L_g+sum([x.L for x in react_data]), + Ag_g=data.A_g+sum([x.A for x in react_data]), + K_g=data.K_g, + Sg_h=data.S_h, + Bg_h=data.B_h, + Eg_h=data.E_h, + Lg_h=data.L_h, + Ag_h=data.A_h, + Sh_h=sum([x.S for x in react_data]), + Bh_h=sum([x.B for x in react_data]), + Eh_h=sum([x.E for x in react_data]), + Lh_h=sum([x.L for x in react_data]), + Ah_h=sum([x.A for x in react_data]), + K_h=data.K_h,comment=data.comment) + class DataCountGAV(object): """ A class for storing the number of data used to fit each solute parameter group value in the solute group additivity. @@ -1196,6 +1494,9 @@ def get_solute_data_from_groups(self, species): by gas-phase thermo estimate. """ molecule = species.molecule[0] + if molecule.contains_surface_site(): + molecule = molecule.get_desorbed_molecules()[0] + molecule.saturate_unfilled_valence() molecule.clear_labeled_atoms() molecule.update_atomtypes() solute_data = self.estimate_solute_via_group_additivity(molecule) @@ -1306,6 +1607,7 @@ def estimate_radical_solute_data_via_hbi(self, molecule, stable_solute_data_esti saturated_struct.remove_bond(bond) saturated_struct.remove_atom(H) atom.increment_radical() + saturated_struct.update_charge() # we need to update charges before updating lone pairs saturated_struct.update() try: self._add_group_solute_data(solute_data, self.groups['radical'], saturated_struct, {'*': atom}) @@ -1717,7 +2019,7 @@ def _add_ring_correction_solute_data_from_tree(self, solute_data, ring_database, entry = ring_database.descend_tree(molecule, atoms) matched_ring_entries.append(entry) - if matched_ring_entries is []: + if matched_ring_entries == []: raise KeyError('Node not found in database.') # Decide which group to keep is_partial_match = True @@ -2012,30 +2314,33 @@ def get_T_dep_solvation_energy_from_LSER_298(self, solute_data, solvent_data, T) return self.get_T_dep_solvation_energy_from_input_298(delG298, delH298, delS298, solvent_name, T) - def get_T_dep_solvation_energy_from_input_298(self, delG298, delH298, delS298, solvent_name, T): - """Returns solvation free energy and K-factor for the input temperature based on the given solvation properties - values at 298 K. - - Args: - delG298 (float): solvation free energy at 298 K in J/mol. - delH298 (float): solvation enthalpy at 298 K in J/mol. - delS298 (float): solvation entropy at 298 K in J/mol/K. - solvent_name (str): name of the solvent that is used in CoolProp. - T (float): input temperature in K. - - Returns: - delG (float): solvation free energy at the input temperature in J/mol. - Kfactor (float): K-factor at the input temperature. K-factor is defined as a ratio of the mole fraction - of a solute in a gas-phase to the mole fraction of a solute in a liquid-phase at equilibrium - - """ - - Kfactor = self.get_Kfactor(delG298, delH298, delS298, solvent_name, T) - rho_g = get_gas_saturation_density(solvent_name, T) - rho_l = get_liquid_saturation_density(solvent_name, T) - delG = constants.R * T * math.log(Kfactor * rho_g / (rho_l)) # in J/mol - return delG, Kfactor - + # def get_T_dep_solvation_energy_from_input_298(self, delG298, delH298, delS298, solvent_name, T): + # """Returns solvation free energy and K-factor for the input temperature based on the given solvation properties + # values at 298 K. + # + # Args: + # delG298 (float): solvation free energy at 298 K in J/mol. + # delH298 (float): solvation enthalpy at 298 K in J/mol. + # delS298 (float): solvation entropy at 298 K in J/mol/K. + # solvent_name (str): name of the solvent that is used in CoolProp. + # T (float): input temperature in K. + # + # Returns: + # delG (float): solvation free energy at the input temperature in J/mol. + # Kfactor (float): K-factor at the input temperature. K-factor is defined as a ratio of the mole fraction + # of a solute in a gas-phase to the mole fraction of a solute in a liquid-phase at equilibrium + # kH (float): the Henry's law constant at the input temperature. kH is defined as the ratio of the pressure + # of a solute in the gas-phase to the concentration of a solute in the liquid-phase at equilibrium. + # """ + # + # Kfactor = self.get_Kfactor(delG298, delH298, delS298, solvent_name, T) + # rho_g = get_gas_saturation_density(solvent_name, T) # saturated gas phase density of the solvent, in mol/m^3 + # rho_l = get_liquid_saturation_density(solvent_name, T) # saturated liquid phase density of the solvent, in mol/m^3 + # Psat_g = get_gas_saturation_pressure(solvent_name, T) + # kH = Kfactor * Psat_g / rho_l # Henry's law constant as a fraction of the gas-phase partial pressure of solute to the liquid-phase concentration of solute + # delG = constants.R * T * math.log(Kfactor * rho_g / (rho_l)) # in J/mol + # return delG, Kfactor, kH + # # def get_Kfactor_parameters(self, delG298, delH298, delS298, solvent_name, T_trans_factor=0.75): # """Returns fitted parameters for the K-factor piecewise functions given the solvation properties at 298 K. # @@ -2142,7 +2447,7 @@ def check_solvent_in_initial_species(self, rmg, solvent_structure): if not any([spec.is_solvent for spec in rmg.initial_species]): if solvent_structure is not None: logging.info('One of the initial species must be the solvent') - raise ValueError('One of the initial species must be the solvent') + logging.warning("Solvent is not an initial species") else: logging.info('One of the initial species must be the solvent with the same string name') - raise ValueError('One of the initial species must be the solvent with the same string name') + logging.warning("Solvent is not an initial species with the same string name") diff --git a/molecule/data/thermo.py b/molecule/data/thermo.py index d0a1c2b..44519b0 100644 --- a/molecule/data/thermo.py +++ b/molecule/data/thermo.py @@ -51,8 +51,6 @@ from molecule.data.surface import MetalDatabase from molecule import settings from molecule.molecule.fragment import Fragment -from molecule.data.surface import MetalDatabase -from molecule import settings #: This dictionary is used to add multiplicity to species label _multiplicity_labels = {1: 'S', 2: 'D', 3: 'T', 4: 'Q', 5: 'V'} @@ -292,7 +290,7 @@ def average_thermo_data(thermo_data_list=None): averaged_thermo_data.Cpdata.value_si[i] /= num_values cp_data = [thermo_data.Cpdata.value_si[i] for thermo_data in thermo_data_list] - averaged_thermo_data.Cpdata.uncertainty[i] = 2 * np.std(cp_data, ddof=1) + averaged_thermo_data.Cpdata.uncertainty_si[i] = 2 * np.std(cp_data, ddof=1) h_data = [thermo_data.H298.value_si for thermo_data in thermo_data_list] averaged_thermo_data.H298.value_si /= num_values @@ -610,7 +608,6 @@ def load_entry(self, index, label, molecule, thermo, reference=None, referenceTy """ mol = Molecule().from_adjacency_list(molecule) mol.update_atomtypes() - entry = Entry( index=index, label=label, @@ -669,7 +666,6 @@ def load_entry(self, molecule = Molecule().from_adjacency_list(molecule) except TypeError: molecule = Fragment().from_adjacency_list(molecule) - molecule.update_atomtypes() # Internal checks for adding entry to the thermo library @@ -818,7 +814,10 @@ def remove_group(self, group_to_remove): groups we also, need to re-point any unicode thermo_data that may have pointed to the entry. - Returns the removed group + This is not (as of 2024/03) used within RMG-Py, but could be useful for + people making ancillary scripts to manipulate or edit the database. + + Returns the removed group. """ # First call base class method @@ -856,6 +855,7 @@ def __init__(self): self.libraries = {} self.surface = {} self.groups = {} + self.adsorption_groups = "adsorptionPt111" self.library_order = [] self.local_context = { 'ThermoData': ThermoData, @@ -991,7 +991,9 @@ def load_groups(self, path): 'longDistanceInteraction_cyclic', 'longDistanceInteraction_noncyclic', 'adsorptionPt111', + 'adsorptionLi' ] + # categories.append(self.adsorption_groups) self.groups = { category: ThermoGroups(label=category).load(os.path.join(path, category + '.py'), self.local_context, self.global_context) @@ -1289,7 +1291,9 @@ def get_thermo_data(self, species, metal_to_scale_to=None, training_set=None): if species.contains_surface_site(): try: thermo0 = self.get_thermo_data_for_surface_species(species) - thermo0 = self.correct_binding_energy(thermo0, species, metal_to_scale_from="Pt111", metal_to_scale_to=metal_to_scale_to) # group adsorption values come from Pt111 + metal_to_scale_from = self.adsorption_groups.split('adsorption')[-1] + if metal_to_scale_from != metal_to_scale_to: + thermo0 = self.correct_binding_energy(thermo0, species, metal_to_scale_from=metal_to_scale_from, metal_to_scale_to=metal_to_scale_to) # group adsorption values come from Pt111 return thermo0 except: logging.error("Error attempting to get thermo for species %s with structure \n%s", @@ -1483,10 +1487,14 @@ def correct_binding_energy(self, thermo, species, metal_to_scale_from=None, meta 'H': molecule.quantity.Energy(0.0, 'eV/molecule'), 'O': molecule.quantity.Energy(0.0, 'eV/molecule'), 'N': molecule.quantity.Energy(0.0, 'eV/molecule'), + 'F': molecule.quantity.Energy(0.0, 'eV/molecule'), } for element, delta_energy in delta_atomic_adsorption_energy.items(): - delta_energy.value_si = metal_to_scale_to_binding_energies[element].value_si - metal_to_scale_from_binding_energies[element].value_si + try: + delta_energy.value_si = metal_to_scale_to_binding_energies[element].value_si - metal_to_scale_from_binding_energies[element].value_si + except KeyError: + pass if all(-0.01 < v.value_si < 0.01 for v in delta_atomic_adsorption_energy.values()): return thermo @@ -1498,7 +1506,7 @@ def correct_binding_energy(self, thermo, species, metal_to_scale_from=None, meta if atom.is_surface_site(): surface_sites.append(atom) normalized_bonds = {'C': 0., 'O': 0., 'N': 0., 'H': 0., 'F': 0., 'Li': 0.} - max_bond_order = {'C': 4., 'O': 2., 'N': 3., 'H': 1., 'F': 1., 'Li': 1.} + max_bond_order = {'C': 4., 'O': 2., 'N': 3., 'H': 1., 'F': 1, 'Li': 1.} for site in surface_sites: numbonds = len(site.bonds) if numbonds == 0: @@ -1528,12 +1536,16 @@ def correct_binding_energy(self, thermo, species, metal_to_scale_from=None, meta # now edit the adsorptionThermo using LSR comments = [] - for element in 'CHON': - if normalized_bonds[element]: - change_in_binding_energy = delta_atomic_adsorption_energy[element].value_si * normalized_bonds[element] - thermo.H298.value_si += change_in_binding_energy - comments.append(f'{normalized_bonds[element]:.2f}{element}') - thermo.comment += " Binding energy corrected by LSR ({}) from {}".format('+'.join(comments), metal_to_scale_from) + change_in_binding_energy = 0 + for element, bond in normalized_bonds.items(): + if bond: + try: + change_in_binding_energy += delta_atomic_adsorption_energy[element].value_si * bond + except KeyError: + continue + comments.append(f'{bond:.2f}{element}') + thermo.H298.value_si += change_in_binding_energy + thermo.comment += f" Binding energy corrected by LSR ({'+'.join(comments)}) from {metal_to_scale_from} (H={change_in_binding_energy/1e3:+.0f}kJ/mol)" return thermo def get_thermo_data_for_surface_species(self, species): @@ -1547,91 +1559,112 @@ def get_thermo_data_for_surface_species(self, species): Returns a :class:`ThermoData` object, with no Cp0 or CpInf """ + # define the comparison function to find the lowest energy + def species_enthalpy(species): + if hasattr(species.thermo, 'H298'): + return species.thermo.H298.value_si + else: + return species.thermo.get_enthalpy(298.0) + if species.is_surface_site(): raise DatabaseError("Can't estimate thermo of vacant site. Should be in library (and should be 0).") logging.debug("Trying to generate thermo for surface species using first of %d resonance isomer(s):", len(species.molecule)) - molecule = species.molecule[0] - # store any labeled atoms to reapply at the end - labeled_atoms = molecule.get_all_labeled_atoms() - molecule.clear_labeled_atoms() - logging.debug("Before removing from surface:\n" + molecule.to_adjacency_list()) - # only want/need to do one resonance structure, - # because will need to regenerate others in gas phase - dummy_molecules = molecule.get_desorbed_molecules() - for mol in dummy_molecules: - mol.clear_labeled_atoms() - if len(dummy_molecules) == 0: - raise RuntimeError(f"Cannot get thermo for gas-phase molecule. No valid dummy molecules from original molecule:\n{molecule.to_adjacency_list()}") + + resonance_data = [] + + # iterate over all resonance structures of the species + for molecule in species.molecule: + # store any labeled atoms to reapply at the end + labeled_atoms = molecule.get_all_labeled_atoms() + molecule.clear_labeled_atoms() + logging.debug("Before removing from surface:\n" + molecule.to_adjacency_list()) + # get all desorbed molecules for a given adsorbate + dummy_molecules = molecule.get_desorbed_molecules() + for mol in dummy_molecules: + mol.clear_labeled_atoms() + if len(dummy_molecules) == 0: + raise RuntimeError(f"Cannot get thermo for gas-phase molecule. No valid dummy molecules from original molecule:\n{molecule.to_adjacency_list()}") - # if len(molecule) > 1, it will assume all resonance structures have already been generated when it tries to generate them, so evaluate each configuration separately and pick the lowest energy one by H298 value - gas_phase_species_from_libraries = [] - gas_phase_species_estimates = [] - for dummy_molecule in dummy_molecules: - dummy_species = Species() - dummy_species.molecule = [dummy_molecule] - dummy_species.generate_resonance_structures() - dummy_species.thermo = self.get_thermo_data(dummy_species) - if dummy_species.thermo.label: - gas_phase_species_from_libraries.append(dummy_species) - else: - gas_phase_species_estimates.append(dummy_species) + # if len(molecule) > 1, it will assume all resonance structures + # have already been generated when it tries to generate them, so + # evaluate each configuration separately and pick the lowest energy + # one by H298 value + gas_phase_species_from_libraries = [] + gas_phase_species_estimates = [] + for dummy_molecule in dummy_molecules: + dummy_species = Species() + dummy_species.molecule = [dummy_molecule] + dummy_species.generate_resonance_structures() + dummy_species.thermo = self.get_thermo_data(dummy_species) + if dummy_species.thermo.label: + gas_phase_species_from_libraries.append(dummy_species) + else: + gas_phase_species_estimates.append(dummy_species) - # define the comparison function to find the lowest energy - def lowest_energy(species): - if hasattr(species.thermo, 'H298'): - return species.thermo.H298.value_si + if gas_phase_species_from_libraries: + gas_phase_species = min(gas_phase_species_from_libraries, key=species_enthalpy) else: - return species.thermo.get_enthalpy(298.0) + gas_phase_species = min(gas_phase_species_estimates, key=species_enthalpy) + + thermo = gas_phase_species.thermo + thermo.comment = f"Gas phase thermo for {thermo.label or gas_phase_species.molecule[0].to_smiles()} from {thermo.comment}. Adsorption correction:" + logging.debug("Using thermo from gas phase for species %s: %r", gas_phase_species.label, thermo) + + if not isinstance(thermo, ThermoData): + thermo = thermo.to_thermo_data() + find_cp0_and_cpinf(gas_phase_species, thermo) + + # Get the adsorption energy + # Create the ThermoData object + adsorption_thermo = ThermoData( + Tdata=([300, 400, 500, 600, 800, 1000, 1500], "K"), + Cpdata=([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], "J/(mol*K)"), + H298=(0.0, "kJ/mol"), + S298=(0.0, "J/(mol*K)"), + ) - if gas_phase_species_from_libraries: - species = min(gas_phase_species_from_libraries, key=lowest_energy) - else: - species = min(gas_phase_species_estimates, key=lowest_energy) + surface_sites = molecule.get_surface_sites() + try: + self._add_adsorption_correction(adsorption_thermo, self.groups['adsorptionPt111'], molecule, surface_sites) + except (KeyError, DatabaseError): + logging.error("Couldn't find in adsorption thermo database:") + logging.error(molecule) + logging.error(molecule.to_adjacency_list()) + raise - thermo = species.thermo - thermo.comment = f"Gas phase thermo for {thermo.label or species.molecule[0].to_smiles()} from {thermo.comment}. Adsorption correction:" - logging.debug("Using thermo from gas phase for species {}\n".format(species.label) + repr(thermo)) + # (group_additivity=True means it appends the comments) + add_thermo_data(thermo, adsorption_thermo, group_additivity=True) - if not isinstance(thermo, ThermoData): - thermo = thermo.to_thermo_data() - find_cp0_and_cpinf(species, thermo) + # if the molecule had labels, reapply them + for label, atom in labeled_atoms.items(): + if isinstance(atom,list): + for a in atom: + a.label = label + else: + atom.label = label - # Get the adsorption energy - # Create the ThermoData object - adsorption_thermo = ThermoData( - Tdata=([300, 400, 500, 600, 800, 1000, 1500], "K"), - Cpdata=([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], "J/(mol*K)"), - H298=(0.0, "kJ/mol"), - S298=(0.0, "J/(mol*K)"), - ) + # a tuple of molecule and its thermo + resonance_data.append((molecule, thermo)) - surface_sites = molecule.get_surface_sites() - try: - self._add_adsorption_correction(adsorption_thermo, self.groups['adsorptionPt111'], molecule, surface_sites) - except (KeyError, DatabaseError): - logging.error("Couldn't find in adsorption thermo database:") - logging.error(molecule) - logging.error(molecule.to_adjacency_list()) - raise + # Get the enthalpy of formation of every adsorbate at 298 K and + # determine the resonance structure with the lowest enthalpy of formation. + # We assume that the lowest enthalpy of formation is the correct + # thermodynamic property for the adsorbate, and the preferred representation. - # (group_additivity=True means it appends the comments) - add_thermo_data(thermo, adsorption_thermo, group_additivity=True) + resonance_data = sorted(resonance_data, key=lambda x: x[1].H298.value_si) + + # reorder the resonance structures (molecules) by their H298 + species.molecule = [mol for mol, thermo in resonance_data] + + thermo = resonance_data[0][1] if thermo.label: thermo.label += 'X' * len(surface_sites) find_cp0_and_cpinf(species, thermo) - # if the molecule had labels, reapply them - for label,atom in labeled_atoms.items(): - if isinstance(atom,list): - for a in atom: - a.label = label - else: - atom.label = label - return thermo def _add_adsorption_correction(self, adsorption_thermo, adsorption_groups, molecule, surface_sites): @@ -2051,8 +2084,12 @@ def estimate_radical_thermo_via_hbi(self, molecule, stable_thermo_estimator): "not {0}".format(thermo_data_sat)) thermo_data_sat = thermo_data_sat[0] else: - thermo_data_sat = stable_thermo_estimator(saturated_struct) - + try: + thermo_data_sat = stable_thermo_estimator(saturated_struct) + except DatabaseError as e: + logging.error(f"Trouble finding thermo data for saturated structure {saturated_struct.to_adjacency_list()}" + f"when trying to evaluate radical {molecule.to_adjacency_list()} via HBI.") + raise e if thermo_data_sat is None: # We couldn't get thermo for the saturated species from libraries, ml, or qm # However, if we were trying group additivity, this could be a problem @@ -2450,7 +2487,7 @@ def _add_ring_correction_thermo_data_from_tree(self, thermo_data, ring_database, entry = ring_database.descend_tree(molecule, atoms) matched_ring_entries.append(entry) - if matched_ring_entries is []: + if not matched_ring_entries: raise KeyError('Node not found in database.') # Decide which group to keep is_partial_match = True @@ -2559,7 +2596,8 @@ def _add_group_thermo_data(self, thermo_data, database, molecule, atom): node = node.parent if node is None: raise DatabaseError(f'Unable to determine thermo parameters for atom {atom} in molecule {molecule}: ' - f'no data for node {node0} or any of its ancestors in database {database.label}.') + f'no data for node {node0} or any of its ancestors in database {database.label}.\n' + + molecule.to_adjacency_list()) data = node.data comment = node.label @@ -2854,7 +2892,6 @@ def register_in_central_thermo_db(self, species): except ValueError: logging.info('Fail to generate inchi/smiles for species below:\n{0}'.format(species.to_adjacency_list())) - def find_cp0_and_cpinf(species, heat_capacity): """ Calculate the Cp0 and CpInf values, and add them to the HeatCapacityModel object. @@ -2865,3 +2902,4 @@ def find_cp0_and_cpinf(species, heat_capacity): if heat_capacity.CpInf is None: cp_inf = species.calculate_cpinf() heat_capacity.CpInf = (cp_inf, "J/(mol*K)") + diff --git a/molecule/kinetics/__init__.py b/molecule/kinetics/__init__.py index 9703826..1cbc9fb 100644 --- a/molecule/kinetics/__init__.py +++ b/molecule/kinetics/__init__.py @@ -29,10 +29,12 @@ from molecule.kinetics.model import KineticsModel, PDepKineticsModel, TunnelingModel, \ get_rate_coefficient_units_from_reaction_order, get_reaction_order_from_rate_coefficient_units -from molecule.kinetics.arrhenius import Arrhenius, ArrheniusEP, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, ArrheniusBM +from molecule.kinetics.arrhenius import Arrhenius, ArrheniusEP, PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, \ + ArrheniusBM, ArrheniusChargeTransfer, ArrheniusChargeTransferBM, Marcus from molecule.kinetics.chebyshev import Chebyshev from molecule.kinetics.falloff import ThirdBody, Lindemann, Troe from molecule.kinetics.kineticsdata import KineticsData, PDepKineticsData from molecule.kinetics.tunneling import Wigner, Eckart from molecule.kinetics.surface import SurfaceArrhenius, SurfaceArrheniusBEP, \ - StickingCoefficient, StickingCoefficientBEP + StickingCoefficient, StickingCoefficientBEP, \ + SurfaceChargeTransfer, SurfaceChargeTransferBEP diff --git a/molecule/kinetics/arrhenius.pxd b/molecule/kinetics/arrhenius.pxd index c743b48..4da601f 100644 --- a/molecule/kinetics/arrhenius.pxd +++ b/molecule/kinetics/arrhenius.pxd @@ -128,4 +128,74 @@ cdef class MultiPDepArrhenius(PDepKineticsModel): cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef change_rate(self, double factor) + +################################################################################ +cdef class ArrheniusChargeTransfer(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ScalarQuantity _Ea + cdef public ScalarQuantity _T0 + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _alpha + cdef public ScalarQuantity _electrons + + cpdef double get_activation_energy_from_potential(self, double V=?, bint non_negative=?) + + cpdef double get_rate_coefficient(self, double T, double V=?) except -1 + cpdef change_rate(self, double factor) + + cpdef change_t0(self, double T0) + + cpdef change_v0(self, double V0) + + cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=?, np.ndarray weights=?, bint three_params=?) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + +################################################################################ + +cdef class ArrheniusChargeTransferBM(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ScalarQuantity _E0 + cdef public ScalarQuantity _w0 + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _alpha + cdef public ScalarQuantity _electrons + + cpdef change_v0(self, double V0) + + cpdef double get_activation_energy(self, double dGrxn) except -1 + + cpdef double get_rate_coefficient_from_potential(self, double T, double V, double dGrxn) except -1 + + cpdef ArrheniusChargeTransfer to_arrhenius_charge_transfer(self, double dGrxn) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef change_rate(self, double factor) + +cdef class Marcus(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ArrayQuantity _lmbd_i_coefs + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _beta + cdef public ScalarQuantity _wr + cdef public ScalarQuantity _wp + cdef public ScalarQuantity _lmbd_o + + cpdef double get_lmbd_i(self, double T) + + cpdef double get_gibbs_activation_energy(self, double T, double dGrxn) except -1 + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef change_rate(self, double factor) \ No newline at end of file diff --git a/molecule/kinetics/arrhenius.pyx b/molecule/kinetics/arrhenius.pyx index 8f3c4ca..da9441f 100644 --- a/molecule/kinetics/arrhenius.pyx +++ b/molecule/kinetics/arrhenius.pyx @@ -37,6 +37,8 @@ import molecule.quantity as quantity from molecule.exceptions import KineticsError from molecule.kinetics.uncertainties import rank_accuracy_map from molecule.molecule.molecule import Bond +from molecule.kinetics.model import KineticsModel, PDepKineticsModel +import logging # Prior to numpy 1.14, `numpy.linalg.lstsq` does not accept None as a value RCOND = -1 if int(np.__version__.split('.')[1]) < 14 else None @@ -58,15 +60,16 @@ cdef class Arrhenius(KineticsModel): `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data `comment` Information about the model (e.g. its source) =============== ============================================================= """ def __init__(self, A=None, n=0.0, Ea=None, T0=(1.0, "K"), Tmin=None, Tmax=None, Pmin=None, Pmax=None, - uncertainty=None, comment=''): + uncertainty=None, solute=None, comment=''): KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, - comment=comment) + solute=solute, comment=comment) self.A = A self.n = n self.Ea = Ea @@ -83,6 +86,7 @@ cdef class Arrhenius(KineticsModel): if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.solute: string += ', solute={0!r}'.format(self.solute) if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) string += ')' return string @@ -92,7 +96,7 @@ cdef class Arrhenius(KineticsModel): A helper function used when pickling an Arrhenius object. """ return (Arrhenius, (self.A, self.n, self.Ea, self.T0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, - self.uncertainty, self.comment)) + self.uncertainty, self.solute, self.comment)) property A: """The preexponential factor.""" @@ -166,12 +170,12 @@ cdef class Arrhenius(KineticsModel): if len(Tlist) < 3 + three_params: raise KineticsError('Not enough degrees of freedom to fit this Arrhenius expression') if three_params: - A = np.zeros((len(Tlist), 3), np.float64) + A = np.zeros((len(Tlist), 3), float) A[:, 0] = np.ones_like(Tlist) A[:, 1] = np.log(Tlist / T0) A[:, 2] = -1.0 / constants.R / Tlist else: - A = np.zeros((len(Tlist), 2), np.float64) + A = np.zeros((len(Tlist), 2), float) A[:, 0] = np.ones_like(Tlist) A[:, 1] = -1.0 / constants.R / Tlist b = np.log(klist) @@ -196,6 +200,7 @@ cdef class Arrhenius(KineticsModel): self.T0 = (T0, "K") self.Tmin = (np.min(Tlist), "K") self.Tmax = (np.max(Tlist), "K") + self.solute = None self.comment = 'Fitted to {0:d} data points; dA = *|/ {1:g}, dn = +|- {2:g}, dEa = +|- {3:g} kJ/mol'.format( len(Tlist), exp(sqrt(cov[0, 0])), @@ -227,58 +232,58 @@ cdef class Arrhenius(KineticsModel): """ self._A.value_si *= factor - def to_cantera_kinetics(self, arrhenius_class=False): - """ - Converts the RMG Arrhenius object to a cantera ArrheniusRate or - the auxiliary cantera Arrhenius class (used by falloff reactions). - Inputs for both are (A,b,E) where A is in units of m^3/kmol/s, b is dimensionless, and E is in J/kmol - - arrhenius_class: If ``True``, uses cantera.Arrhenius (for falloff reactions). If ``False``, uses - Cantera.ArrheniusRate - """ - - import cantera as ct - - rate_units_dimensionality = {'1/s': 0, - 's^-1': 0, - 'm^3/(mol*s)': 1, - 'm^6/(mol^2*s)': 2, - 'cm^3/(mol*s)': 1, - 'cm^6/(mol^2*s)': 2, - 'm^3/(molecule*s)': 1, - 'm^6/(molecule^2*s)': 2, - 'cm^3/(molecule*s)': 1, - 'cm^6/(molecule^2*s)': 2, - } - - if self._T0.value_si != 1: - A = self._A.value_si / (self._T0.value_si) ** self._n.value_si - else: - A = self._A.value_si - - try: - A *= 1000 ** rate_units_dimensionality[self._A.units] - except KeyError: - raise Exception('Arrhenius A-factor units {0} not found among accepted units for converting to ' - 'Cantera Arrhenius object.'.format(self._A.units)) - - b = self._n.value_si - E = self._Ea.value_si * 1000 # convert from J/mol to J/kmol - if arrhenius_class: - return ct.Arrhenius(A, b, E) - else: - return ct.ArrheniusRate(A, b, E) - - def set_cantera_kinetics(self, ct_reaction, species_list): - """ - Passes in a cantera Reaction() object and sets its - rate to a Cantera ArrheniusRate object. - """ - import cantera as ct - assert isinstance(ct_reaction.rate, ct.ArrheniusRate), "Must have a Cantera ArrheniusRate attribute" - - # Set the rate parameter to a cantera Arrhenius object - ct_reaction.rate = self.to_cantera_kinetics() + # def to_cantera_kinetics(self, arrhenius_class=False): + # """ + # Converts the RMG Arrhenius object to a cantera ArrheniusRate or + # the auxiliary cantera Arrhenius class (used by falloff reactions). + # Inputs for both are (A,b,E) where A is in units of m^3/kmol/s, b is dimensionless, and E is in J/kmol + # + # arrhenius_class: If ``True``, uses cantera.Arrhenius (for falloff reactions). If ``False``, uses + # Cantera.ArrheniusRate + # """ + # + # import cantera as ct + # + # rate_units_dimensionality = {'1/s': 0, + # 's^-1': 0, + # 'm^3/(mol*s)': 1, + # 'm^6/(mol^2*s)': 2, + # 'cm^3/(mol*s)': 1, + # 'cm^6/(mol^2*s)': 2, + # 'm^3/(molecule*s)': 1, + # 'm^6/(molecule^2*s)': 2, + # 'cm^3/(molecule*s)': 1, + # 'cm^6/(molecule^2*s)': 2, + # } + # + # if self._T0.value_si != 1: + # A = self._A.value_si / (self._T0.value_si) ** self._n.value_si + # else: + # A = self._A.value_si + # + # try: + # A *= 1000 ** rate_units_dimensionality[self._A.units] + # except KeyError: + # raise Exception('Arrhenius A-factor units {0} not found among accepted units for converting to ' + # 'Cantera Arrhenius object.'.format(self._A.units)) + # + # b = self._n.value_si + # E = self._Ea.value_si * 1000 # convert from J/mol to J/kmol + # if arrhenius_class: + # return ct.Arrhenius(A, b, E) + # else: + # return ct.ArrheniusRate(A, b, E) + # + # def set_cantera_kinetics(self, ct_reaction, species_list): + # """ + # Passes in a cantera Reaction() object and sets its + # rate to a Cantera ArrheniusRate object. + # """ + # import cantera as ct + # assert isinstance(ct_reaction.rate, ct.ArrheniusRate), "Must have a Cantera ArrheniusRate attribute" + # + # # Set the rate parameter to a cantera Arrhenius object + # ct_reaction.rate = self.to_cantera_kinetics() cpdef ArrheniusEP to_arrhenius_ep(self, double alpha=0.0, double dHrxn=0.0): """ @@ -301,6 +306,7 @@ cdef class Arrhenius(KineticsModel): Pmin=self.Pmin, Pmax=self.Pmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment) return aep ################################################################################ @@ -323,15 +329,16 @@ cdef class ArrheniusEP(KineticsModel): `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data `comment` Information about the model (e.g. its source) =============== ============================================================= """ def __init__(self, A=None, n=0.0, alpha=0.0, E0=None, Tmin=None, Tmax=None, Pmin=None, Pmax=None, uncertainty=None, - comment=''): + solute=None, comment=''): KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, - comment=comment) + solute=solute, comment=comment) self.A = A self.n = n self.alpha = alpha @@ -348,6 +355,7 @@ cdef class ArrheniusEP(KineticsModel): if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) if self.uncertainty is not None: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.solute is not None: string += ', solute={0!r}'.format(self.solute) if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) string += ')' return string @@ -357,7 +365,7 @@ cdef class ArrheniusEP(KineticsModel): A helper function used when pickling an ArrheniusEP object. """ return (ArrheniusEP, (self.A, self.n, self.alpha, self.E0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, - self.uncertainty, self.comment)) + self.uncertainty, self.solute, self.comment)) property A: """The preexponential factor.""" @@ -428,6 +436,7 @@ cdef class ArrheniusEP(KineticsModel): Pmin=self.Pmin, Pmax=self.Pmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment, ) @@ -481,15 +490,16 @@ cdef class ArrheniusBM(KineticsModel): `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data `comment` Information about the model (e.g. its source) =============== ============================================================= """ def __init__(self, A=None, n=0.0, w0=(0.0, 'J/mol'), E0=None, Tmin=None, Tmax=None, Pmin=None, Pmax=None, - uncertainty=None, comment=''): + uncertainty=None, solute=None, comment=''): KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, - comment=comment) + solute=solute, comment=comment) self.A = A self.n = n self.w0 = w0 @@ -506,6 +516,7 @@ cdef class ArrheniusBM(KineticsModel): if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) if self.uncertainty is not None: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.solute is not None: string += ', solute={0!r}'.format(self.solute) if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) string += ')' return string @@ -515,7 +526,7 @@ cdef class ArrheniusBM(KineticsModel): A helper function used when pickling an ArrheniusEP object. """ return (ArrheniusBM, (self.A, self.n, self.w0, self.E0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, - self.uncertainty, self.comment)) + self.uncertainty, self.solute, self.comment)) property A: """The preexponential factor.""" @@ -549,7 +560,7 @@ cdef class ArrheniusBM(KineticsModel): """ Return the rate coefficient in the appropriate combination of m^3, mol, and s at temperature `T` in K and enthalpy of reaction `dHrxn` - in J/mol. + in J/mol, evaluated at 298 K. """ cdef double A, n, Ea Ea = self.get_activation_energy(dHrxn) @@ -560,7 +571,7 @@ cdef class ArrheniusBM(KineticsModel): cpdef double get_activation_energy(self, double dHrxn) except -1: """ Return the activation energy in J/mol corresponding to the given - enthalpy of reaction `dHrxn` in J/mol. + enthalpy of reaction `dHrxn` in J/mol, evaluated at 298 K. """ cdef double w0, E0 E0 = self._E0.value_si @@ -576,7 +587,8 @@ cdef class ArrheniusBM(KineticsModel): cpdef Arrhenius to_arrhenius(self, double dHrxn): """ Return an :class:`Arrhenius` instance of the kinetics model using the - given enthalpy of reaction `dHrxn` to determine the activation energy. + given enthalpy of reaction `dHrxn` (in J/mol, evaluated at 298 K) + to determine the activation energy. """ return Arrhenius( A=self.A, @@ -586,6 +598,7 @@ cdef class ArrheniusBM(KineticsModel): Tmin=self.Tmin, Tmax=self.Tmax, uncertainty=self.uncertainty, + solute=self.solute, comment=self.comment, ) @@ -593,6 +606,9 @@ cdef class ArrheniusBM(KineticsModel): """ Fit an ArrheniusBM model to a list of reactions at the given temperatures, w0 must be either given or estimated using the family object + + WARNING: there's a lot of code duplication with ArrheniusChargeTransferBM.fit_to_reactions + so anything you change here you should probably change there too and vice versa! """ assert w0 is not None or recipe is not None, 'either w0 or recipe must be specified' @@ -604,28 +620,25 @@ cdef class ArrheniusBM(KineticsModel): w0 = sum(w0s) / len(w0s) if len(rxns) == 1: - T = 1000.0 rxn = rxns[0] - dHrxn = rxn.get_enthalpy_of_reaction(T) + dHrxn = rxn.get_enthalpy_of_reaction(298.0) A = rxn.kinetics.A.value_si n = rxn.kinetics.n.value_si Ea = rxn.kinetics.Ea.value_si - + def kfcn(E0): Vp = 2 * w0 * (2 * w0 + 2 * E0) / (2 * w0 - 2 * E0) out = Ea - (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) * (Vp - 2 * w0 + dHrxn) / (Vp * Vp - (2 * w0) * (2 * w0) + dHrxn * dHrxn) return out - if abs(dHrxn) > 4 * w0 / 10.0: - E0 = w0 / 10.0 - else: - E0 = fsolve(kfcn, w0 / 10.0)[0] + E0 = fsolve(kfcn, w0 / 10.0)[0] self.Tmin = rxn.kinetics.Tmin self.Tmax = rxn.kinetics.Tmax - self.comment = 'Fitted to {0} reaction at temperature: {1} K'.format(len(rxns), T) + self.solute = None + self.comment = 'Fitted to 1 reaction.' else: - # define optimization function + # define optimization function def kfcn(xs, lnA, n, E0): T = xs[:,0] dHrxn = xs[:,1] @@ -634,7 +647,7 @@ cdef class ArrheniusBM(KineticsModel): Ea = np.where(dHrxn< -4.0*E0, 0.0, Ea) Ea = np.where(dHrxn > 4.0*E0, dHrxn, Ea) return lnA + np.log(T ** n * np.exp(-Ea / (8.314 * T))) - + # get (T,dHrxn(T)) -> (Ln(k) mappings xdata = [] ydata = [] @@ -643,25 +656,24 @@ cdef class ArrheniusBM(KineticsModel): # approximately correct the overall uncertainties to std deviations s = rank_accuracy_map[rxn.rank].value_si/2.0 for T in Ts: - xdata.append([T, rxn.get_enthalpy_of_reaction(T)]) + xdata.append([T, rxn.get_enthalpy_of_reaction(298.0)]) ydata.append(np.log(rxn.get_rate_coefficient(T))) - sigmas.append(s / (8.314 * T)) xdata = np.array(xdata) ydata = np.array(ydata) # fit parameters - boo = True + keep_trying = True xtol = 1e-8 ftol = 1e-8 - while boo: - boo = False + while keep_trying: + keep_trying = False try: params = curve_fit(kfcn, xdata, ydata, sigma=sigmas, p0=[1.0, 1.0, w0 / 10.0], xtol=xtol, ftol=ftol) except RuntimeError: if xtol < 1.0: - boo = True + keep_trying = True xtol *= 10.0 ftol *= 10.0 else: @@ -672,11 +684,14 @@ cdef class ArrheniusBM(KineticsModel): self.Tmin = (np.min(Ts), "K") self.Tmax = (np.max(Ts), "K") + self.solute = None self.comment = 'Fitted to {0} reactions at temperatures: {1}'.format(len(rxns), Ts) # fill in parameters A_units = ['', 's^-1', 'm^3/(mol*s)', 'm^6/(mol^2*s)'] order = len(rxns[0].reactants) + if order != 1 and rxn.is_surface_reaction(): + raise NotImplementedError("Units not implemented for surface reactions.") self.A = (A, A_units[order]) self.n = n @@ -707,12 +722,49 @@ cdef class ArrheniusBM(KineticsModel): """ self._A.value_si *= factor - def set_cantera_kinetics(self, ct_reaction, species_list): - """ - Sets a cantera Reaction() object with the modified Arrhenius object - converted to an Arrhenius form. - """ - raise NotImplementedError('set_cantera_kinetics() is not implemented for ArrheniusBM class kinetics.') + # def to_cantera_kinetics(self): + # """ + # Converts the RMG ArrheniusBM object to a cantera BlowersMaselRate. + # + # BlowersMaselRate(A, b, Ea, W) where A is in units of m^3/kmol/s, + # b is dimensionless, and Ea and W are in J/kmol + # """ + # import cantera as ct + # + # rate_units_conversion = {'1/s': 1, + # 's^-1': 1, + # 'm^3/(mol*s)': 1000, + # 'm^6/(mol^2*s)': 1000000, + # 'cm^3/(mol*s)': 1000, + # 'cm^6/(mol^2*s)': 1000000, + # 'm^3/(molecule*s)': 1000, + # 'm^6/(molecule^2*s)': 1000000, + # 'cm^3/(molecule*s)': 1000, + # 'cm^6/(molecule^2*s)': 1000000, + # } + # + # A = self._A.value_si + # + # try: + # A *= rate_units_conversion[self._A.units] # convert from /mol to /kmol + # except KeyError: + # raise ValueError(f'ArrheniusBM A-factor units {self._A.units} not found among accepted ' + # 'units for converting to Cantera BlowersMaselRate object.') + # + # b = self._n.value_si + # Ea = self._E0.value_si * 1000 # convert from J/mol to J/kmol + # w = self._w0.value_si * 1000 # convert from J/mol to J/kmol + # + # return ct.BlowersMaselRate(A, b, Ea, w) + # + # def set_cantera_kinetics(self, ct_reaction, species_list): + # """ + # Accepts a cantera Reaction object and sets its rate to a Cantera BlowersMaselRate object. + # """ + # import cantera as ct + # if not isinstance(ct_reaction.rate, ct.BlowersMaselRate): + # raise TypeError("ct_reaction must have a cantera BlowersMaselRate as the rate attribute") + # ct_reaction.rate = self.to_cantera_kinetics() ################################################################################ @@ -862,20 +914,20 @@ cdef class PDepArrhenius(PDepKineticsModel): if self.highPlimit is not None: self.highPlimit.change_rate(factor) - def set_cantera_kinetics(self, ct_reaction, species_list): - """ - Sets a Cantera PlogReaction()'s `rates` attribute with - A list of tuples containing [(pressure in Pa, cantera arrhenius object), (..)] - """ - import cantera as ct - import copy - assert isinstance(ct_reaction.rate, ct.PlogRate), "Must have a Cantera PlogRate attribute" - - pressures = copy.deepcopy(self._pressures.value_si) - ctArrhenius = [arr.to_cantera_kinetics(arrhenius_class=True) for arr in self.arrhenius] - - new_rates = ct.PlogRate(list(zip(pressures, ctArrhenius))) - ct_reaction.rate = new_rates + # def set_cantera_kinetics(self, ct_reaction, species_list): + # """ + # Sets a Cantera PlogReaction()'s `rates` attribute with + # A list of tuples containing [(pressure in Pa, cantera arrhenius object), (..)] + # """ + # import cantera as ct + # import copy + # assert isinstance(ct_reaction.rate, ct.PlogRate), "Must have a Cantera PlogRate attribute" + # + # pressures = copy.deepcopy(self._pressures.value_si) + # ctArrhenius = [arr.to_cantera_kinetics(arrhenius_class=True) for arr in self.arrhenius] + # + # new_rates = ct.PlogRate(list(zip(pressures, ctArrhenius))) + # ct_reaction.rate = new_rates ################################################################################ @@ -971,7 +1023,7 @@ cdef class MultiArrhenius(KineticsModel): if Tmax == -1: Tmax = self.Tmax.value_si kunits = str(quantity.pq.Quantity(1.0, self.arrhenius[0].A.units).simplified).split()[-1] # is this the best way to get the units returned by k?? Tlist = np.logspace(log10(Tmin), log10(Tmax), num=25) - klist = np.array(list(map(self.get_rate_coefficient, Tlist)), np.float64) + klist = np.array(list(map(self.get_rate_coefficient, Tlist)), float) arrh = Arrhenius().fit_to_data(Tlist, klist, kunits) arrh.comment = "Fitted to Multiple Arrhenius kinetics over range {Tmin}-{Tmax} K. {comment}".format( Tmin=Tmin, Tmax=Tmax, comment=self.comment) @@ -1097,6 +1149,759 @@ cdef class MultiPDepArrhenius(PDepKineticsModel): for i, arr in enumerate(self.arrhenius): arr.set_cantera_kinetics(ct_reaction[i], species_list) +################################################################################ + +cdef class ArrheniusChargeTransfer(KineticsModel): + + """ + A kinetics model for surface charge transfer reactions + + It is very similar to the :class:`SurfaceArrhenius`, but the Ea is potential-dependent + + + The attributes are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `T0` The reference temperature + `n` The temperature exponent + `Ea` The activation energy + `electrons` The stochiometry coeff for electrons (negative if reactant, positive if product) + `V0` The reference potential + `alpha` The charge transfer coefficient + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` The transition state solute data + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, Ea=None, V0=None, alpha=0.5, electrons=-1, T0=(1.0, "K"), Tmin=None, Tmax=None, + Pmin=None, Pmax=None, solute=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, solute=solute, uncertainty=uncertainty, + comment=comment) + + self.alpha = alpha + self.A = A + self.n = n + self.Ea = Ea + self.T0 = T0 + self.electrons = electrons + self.V0 = V0 + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Arrhenius object. + """ + string = 'ArrheniusChargeTransfer(A={0!r}, n={1!r}, Ea={2!r}, V0={3!r}, alpha={4!r}, electrons={5!r}, T0={6!r}'.format( + self.A, self.n, self.Ea, self.V0, self.alpha, self.electrons, self.T0) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.solute: string += ', solute={0!r}'.format(self.solute) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a ArrheniusChargeTransfer object. + """ + return (ArrheniusChargeTransfer, (self.A, self.n, self.Ea, self.V0, self.alpha, self.electrons, self.T0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.solute, self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.SurfaceRateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property Ea: + """The activation energy.""" + def __get__(self): + return self._Ea + def __set__(self, value): + self._Ea = quantity.Energy(value) + + property T0: + """The reference temperature.""" + def __get__(self): + return self._T0 + def __set__(self, value): + self._T0 = quantity.Temperature(value) + + property V0: + """The reference potential.""" + def __get__(self): + return self._V0 + def __set__(self, value): + self._V0 = quantity.Potential(value) + + property electrons: + """The number of electrons transferred.""" + def __get__(self): + return self._electrons + def __set__(self, value): + self._electrons = quantity.Dimensionless(value) + + property alpha: + """The charge transfer coefficient.""" + def __get__(self): + return self._alpha + def __set__(self, value): + self._alpha = quantity.Dimensionless(value) + + cpdef double get_activation_energy_from_potential(self, double V=0.0, bint non_negative=True): + """ + Return the effective activation energy (in J/mol) at specificed potential (in Volts). + """ + cdef double electrons, alpha, Ea, V0 + + electrons = self._electrons.value_si + alpha = self._alpha.value_si + Ea = self._Ea.value_si + V0 = self._V0.value_si + + Ea -= alpha * electrons * constants.F * (V-V0) + + if non_negative is True: + if Ea < 0: + Ea = 0.0 + + return Ea + + cpdef double get_rate_coefficient(self, double T, double V=0.0) except -1: + """ + Return the rate coefficient in the appropriate combination of m^2, + mol, and s at temperature `T` in K. + """ + cdef double A, n, V0, T0, Ea + + A = self._A.value_si + n = self._n.value_si + V0 = self._V0.value_si + T0 = self._T0.value_si + + if V != V0: + Ea = self.get_activation_energy_from_potential(V) + else: + Ea = self._Ea.value_si + + return A * (T / T0) ** n * exp(-Ea / (constants.R * T)) + + cpdef change_t0(self, double T0): + """ + Changes the reference temperature used in the exponent to `T0` in K, + and adjusts the preexponential factor accordingly. + """ + self._A.value_si /= (self._T0.value_si / T0) ** self._n.value_si + self._T0.value_si = T0 + + cpdef change_v0(self, double V0): + """ + Changes the reference potential to `V0` in volts, and adjusts the + activation energy `Ea` accordingly. + """ + + self._Ea.value_si = self.get_activation_energy_from_potential(V0) + self._V0.value_si = V0 + + cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=1, + np.ndarray weights=None, bint three_params=False): + """ + Fit the Arrhenius parameters to a set of rate coefficient data `klist` + in units of `kunits` corresponding to a set of temperatures `Tlist` in + K. A linear least-squares fit is used, which guarantees that the + resulting parameters provide the best possible approximation to the + data. + """ + import scipy.stats + if not all(np.isfinite(klist)): + raise ValueError("Rates must all be finite, not inf or NaN") + if any(klist<0): + if not all(klist<0): + raise ValueError("Rates must all be positive or all be negative.") + rate_sign_multiplier = -1 + klist = -1 * klist + else: + rate_sign_multiplier = 1 + + assert len(Tlist) == len(klist), "length of temperatures and rates must be the same" + if len(Tlist) < 3 + three_params: + raise KineticsError('Not enough degrees of freedom to fit this Arrhenius expression') + if three_params: + A = np.zeros((len(Tlist), 3), np.float64) + A[:, 0] = np.ones_like(Tlist) + A[:, 1] = np.log(Tlist / T0) + A[:, 2] = -1.0 / constants.R / Tlist + else: + A = np.zeros((len(Tlist), 2), np.float64) + A[:, 0] = np.ones_like(Tlist) + A[:, 1] = -1.0 / constants.R / Tlist + b = np.log(klist) + if weights is not None: + for n in range(b.size): + A[n, :] *= weights[n] + b[n] *= weights[n] + x, residues, rank, s = np.linalg.lstsq(A, b, rcond=RCOND) + + # Determine covarianace matrix to obtain parameter uncertainties + count = klist.size + cov = residues[0] / (count - 3) * np.linalg.inv(np.dot(A.T, A)) + t = scipy.stats.t.ppf(0.975, count - 3) + + if not three_params: + x = np.array([x[0], 0, x[1]]) + cov = np.array([[cov[0, 0], 0, cov[0, 1]], [0, 0, 0], [cov[1, 0], 0, cov[1, 1]]]) + + self.A = (rate_sign_multiplier * exp(x[0]), kunits) + self.n = x[1] + self.Ea = (x[2] * 0.001, "kJ/mol") + self.T0 = (T0, "K") + self.Tmin = (np.min(Tlist), "K") + self.Tmax = (np.max(Tlist), "K") + self.solute = None, + self.comment = 'Fitted to {0:d} data points; dA = *|/ {1:g}, dn = +|- {2:g}, dEa = +|- {3:g} kJ/mol'.format( + len(Tlist), + exp(sqrt(cov[0, 0])), + sqrt(cov[1, 1]), + sqrt(cov[2, 2]) * 0.001, + ) + + return self + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, ArrheniusChargeTransfer): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.Ea.equals(other_kinetics.Ea) or not self.T0.equals(other_kinetics.T0) + or not self.alpha.equals(other_kinetics.alpha) or not self.electrons.equals(other_kinetics.electrons) + or not self.V0.equals(other_kinetics.V0)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor in Arrhenius expression by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + +cdef class ArrheniusChargeTransferBM(KineticsModel): + """ + A kinetics model based on the (modified) Arrhenius equation, using the + Evans-Polanyi equation to determine the activation energy. The attributes + are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `n` The temperature exponent + `w0` The average of the bond dissociation energies of the bond formed and the bond broken + `E0` The activation energy for a thermoneutral reaction + `electrons` The stochiometry coeff for electrons (negative if reactant, positive if product) + `V0` The reference potential + `alpha` The charge transfer coefficient + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, w0=(0.0, 'J/mol'), E0=None, V0=(0.0,'V'), alpha=0.5, electrons=-1, Tmin=None, Tmax=None, + Pmin=None, Pmax=None, solute=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, solute=solute, uncertainty=uncertainty, + comment=comment) + + self.alpha = alpha + self.A = A + self.n = n + self.w0 = w0 + self.E0 = E0 + self.electrons = electrons + self.V0 = V0 + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Arrhenius object. + """ + string = 'ArrheniusChargeTransferBM(A={0!r}, n={1!r}, w0={2!r}, E0={3!r}, V0={4!r}, alpha={5!r}, electrons={6!r}'.format( + self.A, self.n, self.w0, self.E0, self.V0, self.alpha, self.electrons) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.solute: string += ', solute={0!r}'.format(self.solute) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a ArrheniusChargeTransfer object. + """ + return (ArrheniusChargeTransferBM, (self.A, self.n, self.w0, self.E0, self.V0, self.alpha, self.electrons, self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.solute, self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.SurfaceRateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property w0: + """The average of the bond dissociation energies of the bond formed and the bond broken.""" + def __get__(self): + return self._w0 + def __set__(self, value): + self._w0 = quantity.Energy(value) + + property E0: + """The activation energy.""" + def __get__(self): + return self._E0 + def __set__(self, value): + self._E0 = quantity.Energy(value) + + property V0: + """The reference potential.""" + def __get__(self): + return self._V0 + def __set__(self, value): + self._V0 = quantity.Potential(value) + + property electrons: + """The number of electrons transferred.""" + def __get__(self): + return self._electrons + def __set__(self, value): + self._electrons = quantity.Dimensionless(value) + + property alpha: + """The charge transfer coefficient.""" + def __get__(self): + return self._alpha + def __set__(self, value): + self._alpha = quantity.Dimensionless(value) + + cpdef change_v0(self, double V0): + """ + Changes the reference potential to `V0` in volts, and adjusts the + activation energy `E0` accordingly. + """ + + self._E0.value_si = self.get_activation_energy_from_potential(V0,0.0) + self._V0.value_si = V0 + + cpdef double get_rate_coefficient(self, double T, double dHrxn=0.0) except -1: + """ + Return the rate coefficient in the appropriate combination of m^3, + mol, and s at temperature `T` in K and enthalpy of reaction `dHrxn` + in J/mol. + """ + cdef double A, n, Ea + Ea = self.get_activation_energy(dHrxn) + A = self._A.value_si + n = self._n.value_si + return A * T ** n * exp(-Ea / (constants.R * T)) + + cpdef double get_activation_energy(self, double dHrxn) except -1: + """ + Return the activation energy in J/mol corresponding to the given + enthalpy of reaction `dHrxn` in J/mol. + """ + cdef double w0, E0 + E0 = self._E0.value_si + if dHrxn < -4 * self._E0.value_si: + return 0.0 + elif dHrxn > 4 * self._E0.value_si: + return dHrxn + else: + w0 = self._w0.value_si + Vp = 2 * w0 * (2 * w0 + 2 * E0) / (2 * w0 - 2 * E0) + return (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) ** 2 / (Vp ** 2 - (2 * w0) ** 2 + dHrxn ** 2) + + cpdef double get_rate_coefficient_from_potential(self, double T, double V, double dHrxn) except -1: + """ + Return the rate coefficient in the appropriate combination of m^3, + mol, and s at temperature `T` in K, potential `V` in volts, and + heat of reaction `dHrxn` in J/mol. + """ + cdef double A, n, Ea + Ea = self.get_activation_energy_from_potential(V,dHrxn) + Ea -= self._alpha.value_si * self._electrons.value_si * constants.F * (V-self._V0.value_si) + A = self._A.value_si + n = self._n.value_si + return A * T ** n * exp(-Ea / (constants.R * T)) + + def fit_to_reactions(self, rxns, w0=None, recipe=None, Ts=None): + """ + Fit an ArrheniusChargeTransferBM model to a list of reactions at the given temperatures, + w0 must be either given or estimated using the family object + + WARNING: there's a lot of code duplication with ArrheniusBM.fit_to_reactions + so anything you change here you should probably change there too and vice versa! + """ + assert w0 is not None or recipe is not None, 'either w0 or recipe must be specified' + + for rxn in rxns: + if rxn.kinetics._V0.value_si != 0.0: + rxn.kinetics.change_v0(0.0) + + if Ts is None: + Ts = [300.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0, 1100.0, 1200.0, 1500.0] + if w0 is None: + #estimate w0 + w0s = get_w0s(recipe, rxns) + w0 = sum(w0s) / len(w0s) + + if len(rxns) == 1: + rxn = rxns[0] + dHrxn = rxn.get_enthalpy_of_reaction(298.0) + A = rxn.kinetics.A.value_si + n = rxn.kinetics.n.value_si + Ea = rxn.kinetics.Ea.value_si + + def kfcn(E0): + Vp = 2 * w0 * (2 * w0 + 2 * E0) / (2 * w0 - 2 * E0) + out = Ea - (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) * (Vp - 2 * w0 + dHrxn) / (Vp * Vp - (2 * w0) * (2 * w0) + dHrxn * dHrxn) + return out + + E0 = fsolve(kfcn, w0 / 10.0)[0] + + self.Tmin = rxn.kinetics.Tmin + self.Tmax = rxn.kinetics.Tmax + self.comment = 'Fitted to 1 reaction' + else: + # define optimization function + def kfcn(xs, lnA, n, E0): + T = xs[:,0] + dHrxn = xs[:,1] + Vp = 2 * w0 * (2 * w0 + 2 * E0) / (2 * w0 - 2 * E0) + Ea = (w0 + dHrxn / 2.0) * (Vp - 2 * w0 + dHrxn) * (Vp - 2 * w0 + dHrxn) / (Vp * Vp - (2 * w0) * (2 * w0) + dHrxn * dHrxn) + Ea = np.where(dHrxn< -4.0*E0, 0.0, Ea) + Ea = np.where(dHrxn > 4.0*E0, dHrxn, Ea) + return lnA + np.log(T ** n * np.exp(-Ea / (8.314 * T))) + + # get (T,dHrxn(T)) -> (Ln(k) mappings + xdata = [] + ydata = [] + sigmas = [] + for rxn in rxns: + # approximately correct the overall uncertainties to std deviations + s = rank_accuracy_map[rxn.rank].value_si/2.0 + for T in Ts: + xdata.append([T, rxn.get_enthalpy_of_reaction(298.0)]) + ydata.append(np.log(rxn.get_rate_coefficient(T))) + sigmas.append(s / (8.314 * T)) + + xdata = np.array(xdata) + ydata = np.array(ydata) + + # fit parameters + keep_trying = True + xtol = 1e-8 + ftol = 1e-8 + while keep_trying: + keep_trying = False + try: + params = curve_fit(kfcn, xdata, ydata, sigma=sigmas, p0=[1.0, 1.0, w0 / 10.0], xtol=xtol, ftol=ftol) + except RuntimeError: + if xtol < 1.0: + keep_trying = True + xtol *= 10.0 + ftol *= 10.0 + else: + raise ValueError("Could not fit BM arrhenius to reactions with xtol<1.0") + + lnA, n, E0 = params[0].tolist() + A = np.exp(lnA) + + self.Tmin = (np.min(Ts), "K") + self.Tmax = (np.max(Ts), "K") + self.comment = 'Fitted to {0} reactions at temperatures: {1}'.format(len(rxns), Ts) + + # fill in parameters + A_units = ['', 's^-1', 'm^3/(mol*s)', 'm^6/(mol^2*s)'] + order = len(rxns[0].reactants) + if order != 1 and rxn.is_surface_reaction(): + raise NotImplementedError("Units not implemented for surface reactions") + self.A = (A, A_units[order]) + + self.n = n + self.w0 = (w0, 'J/mol') + self.E0 = (E0, 'J/mol') + self._V0.value_si = 0.0 + self.electrons = rxns[0].electrons + + return self + + cpdef ArrheniusChargeTransfer to_arrhenius_charge_transfer(self, double dHrxn): + """ + Return an :class:`ArrheniusChargeTransfer` instance of the kinetics model using the + given heat of reaction `dHrxn` to determine the activation energy. + """ + return ArrheniusChargeTransfer( + A=self.A, + n=self.n, + electrons=self.electrons, + Ea=(self.get_activation_energy(dHrxn) * 0.001, "kJ/mol"), + V0=self.V0, + T0=(1, "K"), + Tmin=self.Tmin, + Tmax=self.Tmax, + Pmin=self.Pmin, + Pmax=self.Pmax, + uncertainty=self.uncertainty, + solute=self.solute, + comment=self.comment, + ) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, ArrheniusChargeTransferBM): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.E0.equals(other_kinetics.E0) or not self.w0.equals(other_kinetics.w0) + or not self.alpha.equals(other_kinetics.alpha) + or not self.electrons.equals(other_kinetics.electrons) or not self.V0.equals(other_kinetics.V0)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + + def set_cantera_kinetics(self, ct_reaction, species_list): + """ + Sets a cantera ElementaryReaction() object with the modified Arrhenius object + converted to an Arrhenius form. + """ + raise NotImplementedError('set_cantera_kinetics() is not implemented for ArrheniusEP class kinetics.') + +cdef class Marcus(KineticsModel): + """ + A kinetics model based on the (modified) Arrhenius equation, using the + Evans-Polanyi equation to determine the activation energy. The attributes + are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `n` The temperature exponent + `lmbd_i_coefs` Coefficients for inner sphere reorganization energy + `V0` The reference potential + `beta` Transmission decay coefficient + `wr` Work to bring reactants together + `wp` Work to bring products together + `lmbd_o` Outer sphere reorganization energy (solvent) + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Transition state solute data + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, lmbd_i_coefs=np.array([0.0,0.0,0.0,0.0]), beta=(1.2e-10,"1/m"), + wr=(0,"J/mol"), wp=(0,"J/mol"), lmbd_o=(0,"J/mol"), Tmin=None, Tmax=None, + Pmin=None, Pmax=None, solute=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, solute=solute, uncertainty=uncertainty, + comment=comment) + + self.A = A + self.n = n + self.lmbd_i_coefs = lmbd_i_coefs + self.beta = beta + self.wr = wr + self.wp = wp + self.lmbd_o = lmbd_o + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Marcus object. + """ + string = 'Marcus(A={0!r}, n={1!r}, lmbd_i_coefs={2!r}, beta={3!r}, wr={4!r}, wp={5!r}, lmbd_o={6!r}'.format( + self.A, self.n, self.lmbd_i_coefs, self.beta, self.wr, self.wp, self.lmbd_o) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.solute: string += ', solute={0!r}'.format(self.solute) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a Marcus object. + """ + return (Marcus, (self.A, self.n, self.lmbd_i_coefs, self.beta, self.wr, self.wp, self.lmbd_o, + self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.solute, self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.RateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property lmbd_i_coefs: + """Temperature polynomial coefficients for inner sphere reogranization energy""" + def __get__(self): + return self._lmbd_i_coefs + def __set__(self, value): + self._lmbd_i_coefs = quantity.Dimensionless(value) + + property beta: + """transmission coefficient""" + def __get__(self): + return self._beta + def __set__(self, value): + self._beta = quantity.UnitType('m^-1')(value) + + property lmbd_o: + """outer sphere reorganization energy""" + def __get__(self): + return self._lmbd_o + def __set__(self, value): + self._lmbd_o = quantity.Energy(value) + + property wr: + """outer sphere reorganization energy""" + def __get__(self): + return self._wr + def __set__(self, value): + self._wr = quantity.Energy(value) + + property wp: + """outer sphere reorganization energy""" + def __get__(self): + return self._wp + def __set__(self, value): + self._wp = quantity.Energy(value) + + cpdef double get_rate_coefficient(self, double T, double dGrxn=0.0) except -1: + """ + Return the rate coefficient in the appropriate combination of m^3, + mol, and s at temperature `T` in K and enthalpy of reaction `dHrxn` + in J/mol. + """ + cdef double A, n, dG + dG = self.get_gibbs_activation_energy(T, dGrxn) + A = self._A.value_si + n = self._n.value_si + return A * T ** n * exp(-dG / (constants.R * T)) + + cpdef double get_lmbd_i(self, double T): + """ + Return lmbd_i in J/mol + """ + return self.lmbd_i_coefs.value_si[0]+self.lmbd_i_coefs.value_si[1]*T+self.lmbd_i_coefs.value_si[2]*T**2+self.lmbd_i_coefs.value_si[3]*T**3 + + cpdef double get_gibbs_activation_energy(self, double T, double dGrxn) except -1: + """ + Return the activation energy in J/mol corresponding to the given + enthalpy of reaction `dHrxn` in J/mol. + """ + cdef double lmbd_i + lmbd_i = self.get_lmbd_i(T) + return (lmbd_i+self.lmbd_o.value_si)/4.0*(1.0+dGrxn/(lmbd_i+self.lmbd_o.value_si))**2 + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, Marcus): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.lmbd_i_coefs.equals(other_kinetics.lmbd_i_coefs) or not self.lmbd_o.equals(other_kinetics.lmbd_o) + or not self.beta.equals(other_kinetics.beta) + or not self.electrons.equals(other_kinetics.electrons)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + + def set_cantera_kinetics(self, ct_reaction, species_list): + """ + Sets a cantera ElementaryReaction() object with the modified Arrhenius object + converted to an Arrhenius form. + """ + raise NotImplementedError('set_cantera_kinetics() is not implemented for Marcus class kinetics.') + def get_w0(actions, rxn): """ calculates the w0 for Blower Masel kinetics by calculating wf (total bond energy of bonds formed) @@ -1167,4 +1972,4 @@ def get_w0(actions, rxn): return (wf + wb) / 2.0 def get_w0s(actions, rxns): - return [get_w0(actions, rxn) for rxn in rxns] + return [get_w0(actions, rxn) for rxn in rxns] \ No newline at end of file diff --git a/molecule/kinetics/chebyshev.pyx b/molecule/kinetics/chebyshev.pyx index a50b38b..cdedd0b 100644 --- a/molecule/kinetics/chebyshev.pyx +++ b/molecule/kinetics/chebyshev.pyx @@ -212,8 +212,8 @@ cdef class Chebyshev(PDepKineticsModel): K = quantity.RateCoefficient(K, kunits).value_si # Create matrix and vector for coefficient fit (linear least-squares) - A = np.zeros((nT * nP, degreeT * degreeP), np.float64) - b = np.zeros((nT * nP), np.float64) + A = np.zeros((nT * nP, degreeT * degreeP), float) + b = np.zeros((nT * nP), float) for t1, T in enumerate(Tred): for p1, P in enumerate(Pred): for t2 in range(degreeT): @@ -225,7 +225,7 @@ cdef class Chebyshev(PDepKineticsModel): x, residues, rank, s = np.linalg.lstsq(A, b, rcond=RCOND) # Extract coefficients - coeffs = np.zeros((degreeT, degreeP), np.float64) + coeffs = np.zeros((degreeT, degreeP), float) for t2 in range(degreeT): for p2 in range(degreeP): coeffs[t2, p2] = x[p2 * degreeT + t2] diff --git a/molecule/kinetics/falloff.pyx b/molecule/kinetics/falloff.pyx index 3d78d22..82d43b2 100644 --- a/molecule/kinetics/falloff.pyx +++ b/molecule/kinetics/falloff.pyx @@ -220,15 +220,12 @@ cdef class Lindemann(PDepKineticsModel): self.arrheniusLow.change_rate(factor) self.arrheniusHigh.change_rate(factor) - - def set_cantera_kinetics(self, ct_reaction, species_list): """ Sets the efficiencies and kinetics for a cantera reaction. """ import cantera as ct assert isinstance(ct_reaction.rate, ct.LindemannRate), "Must have a Cantera LindemannRate attribute" - ct_reaction.efficiencies = PDepKineticsModel.get_cantera_efficiencies(self, species_list) ct_reaction.rate = self.to_cantera_kinetics() @@ -400,11 +397,8 @@ cdef class Troe(PDepKineticsModel): for a cantera FalloffReaction. """ import cantera as ct - assert isinstance(ct_reaction.rate, ct.TroeRate), "Must have a Cantera TroeRate attribute" - ct_reaction.efficiencies = PDepKineticsModel.get_cantera_efficiencies(self, species_list) - ct_reaction.rate = self.to_cantera_kinetics() def to_cantera_kinetics(self): @@ -424,7 +418,3 @@ cdef class Troe(PDepKineticsModel): high = self.arrheniusHigh.to_cantera_kinetics(arrhenius_class=True) low = self.arrheniusLow.to_cantera_kinetics(arrhenius_class=True) return ct.TroeRate(high=high, low=low, falloff_coeffs=falloff) - - - - \ No newline at end of file diff --git a/molecule/kinetics/kineticsdata.pyx b/molecule/kinetics/kineticsdata.pyx index c224b85..2b3f273 100644 --- a/molecule/kinetics/kineticsdata.pyx +++ b/molecule/kinetics/kineticsdata.pyx @@ -29,7 +29,7 @@ import numpy as np cimport numpy as np -from libc.math cimport log +from libc.math cimport log, pow import molecule.quantity as quantity @@ -115,12 +115,10 @@ cdef class KineticsData(KineticsModel): raise ValueError('Unable to compute rate coefficient at {0:g} K using KineticsData model.'.format(T)) else: for i in range(N - 1): - Tlow = Tdata[i] - Thigh = Tdata[i + 1] - if Tlow <= T and T <= Thigh: - klow = kdata[i] - khigh = kdata[i + 1] - k = klow * (khigh / klow) ** ((T - Tlow) / (Thigh - Tlow)) + Tlow, Thigh = Tdata[i], Tdata[i + 1] + if Tlow <= T <= Thigh: + klow, khigh = kdata[i], kdata[i + 1] + k = klow * pow(khigh / klow, (T - Tlow) / (Thigh - Tlow)) break return k @@ -208,54 +206,48 @@ cdef class PDepKineticsData(PDepKineticsModel): cpdef double get_rate_coefficient(self, double T, double P=0.0) except -1: """ - Return the rate coefficient in the appropriate combination of m^3, - mol, and s at temperature `T` in K and pressure `P` in Pa. + Return the rate coefficient in m^3, mol, and s at temperature T (K) + and pressure P (Pa). """ - cdef np.ndarray[np.float64_t, ndim=1] Tdata, Pdata - cdef np.ndarray[np.float64_t, ndim=2] kdata - cdef double Tlow, Thigh, Plow, Phigh, klow, khigh - cdef double k - cdef int i, j, M, N - - if P == 0: - raise ValueError('No pressure specified to pressure-dependent PDepKineticsData.get_rate_coefficient().') - - Tdata = self._Tdata.value_si - Pdata = self._Pdata.value_si - kdata = self._kdata.value_si - M = kdata.shape[0] - N = kdata.shape[1] - k = 0.0 + # Use C memoryviews so indexing yields raw C doubles + cdef double[:] Tview = self._Tdata.value_si + cdef double[:] Pview = self._Pdata.value_si + cdef double[:, :] Kview = self._kdata.value_si + cdef double Tlow, Thigh, Plow, Phigh, klow, khigh, kout = 0.0 + cdef int i, j, M = Tview.shape[0], N = Pview.shape[0] - # Make sure we are interpolating and not extrapolating - if T < Tdata[0]: - raise ValueError( - 'Unable to compute rate coefficient at {0:g} K and {1:g} Pa using PDepKineticsData model.'.format(T, P)) - elif T > Tdata[M - 1]: - raise ValueError( - 'Unable to compute rate coefficient at {0:g} K and {1:g} Pa using PDepKineticsData model.'.format(T, P)) - if P < Pdata[0]: - raise ValueError( - 'Unable to compute rate coefficient at {0:g} K and {1:g} Pa using PDepKineticsData model.'.format(T, P)) - elif P > Pdata[N - 1]: + if P == 0.0: raise ValueError( - 'Unable to compute rate coefficient at {0:g} K and {1:g} Pa using PDepKineticsData model.'.format(T, P)) - else: - for i in range(M - 1): - Tlow = Tdata[i] - Thigh = Tdata[i + 1] - if Tlow <= T and T <= Thigh: - for j in range(N - 1): - Plow = Pdata[j] - Phigh = Pdata[j + 1] - if Plow <= P and P <= Phigh: - klow = kdata[i, j] * (kdata[i + 1, j] / kdata[i, j]) ** ((T - Tlow) / (Thigh - Tlow)) - khigh = kdata[i, j + 1] * (kdata[i + 1, j + 1] / kdata[i, j + 1]) ** ( - (T - Tlow) / (Thigh - Tlow)) - k = klow * (khigh / klow) ** (log(P / Plow) / log(Phigh / Plow)) - break + "No pressure specified to pressure-dependent get_rate_coefficient()." + ) + if not (Tview[0] <= T <= Tview[M - 1] and Pview[0] <= P <= Pview[N - 1]): + raise ValueError(f"Conditions ({T:g} K, {P:g} Pa) out of range.") + + for i in range(M - 1): + Tlow = Tview[i] + Thigh = Tview[i + 1] + if Tlow <= T <= Thigh: + for j in range(N - 1): + Plow = Pview[j] + Phigh = Pview[j + 1] + if Plow <= P <= Phigh: + # interpolate in temperature + klow = Kview[i, j] * pow( + Kview[i + 1, j] / Kview[i, j], + (T - Tlow) / (Thigh - Tlow), + ) + khigh = Kview[i, j + 1] * pow( + Kview[i + 1, j + 1] / Kview[i, j + 1], + (T - Tlow) / (Thigh - Tlow), + ) + # then interpolate in pressure + kout = klow * pow( + khigh / klow, + log(P / Plow) / log(Phigh / Plow), + ) + return kout + return kout - return k cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: """ diff --git a/molecule/kinetics/model.pxd b/molecule/kinetics/model.pxd index e369102..6848fe6 100644 --- a/molecule/kinetics/model.pxd +++ b/molecule/kinetics/model.pxd @@ -29,7 +29,7 @@ cimport numpy as np from molecule.quantity cimport ScalarQuantity, ArrayQuantity from molecule.kinetics.uncertainties cimport RateUncertainty - +from molecule.data.solvation import SoluteData ################################################################################ cpdef str get_rate_coefficient_units_from_reaction_order(n_gas, n_surf=?) @@ -43,6 +43,7 @@ cdef class KineticsModel: cdef public ScalarQuantity _Tmin, _Tmax cdef public ScalarQuantity _Pmin, _Pmax cdef public RateUncertainty uncertainty + cdef public object solute cdef public str comment diff --git a/molecule/kinetics/model.pyx b/molecule/kinetics/model.pyx index e43051c..0182383 100644 --- a/molecule/kinetics/model.pyx +++ b/molecule/kinetics/model.pyx @@ -38,6 +38,8 @@ from libc.math cimport log10 import molecule.quantity as quantity from molecule.molecule import Molecule +from molecule.kinetics.surface import StickingCoefficient, StickingCoefficientBEP + ################################################################################ @@ -117,16 +119,18 @@ cdef class KineticsModel: `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `solute` Solute data for the transition state `comment` Information about the model (e.g. its source) =============== ============================================================ """ - def __init__(self, Tmin=None, Tmax=None, Pmin=None, Pmax=None, uncertainty=None, comment=''): + def __init__(self, Tmin=None, Tmax=None, Pmin=None, Pmax=None, uncertainty=None, solute=None, comment=''): self.Tmin = Tmin self.Tmax = Tmax self.Pmin = Pmin self.Pmax = Pmax + self.solute = solute self.uncertainty = uncertainty self.comment = comment @@ -135,14 +139,14 @@ cdef class KineticsModel: Return a string representation that can be used to reconstruct the KineticsModel object. """ - return 'KineticsModel(Tmin={0!r}, Tmax={1!r}, Pmin={2!r}, Pmax={3!r}, uncertainty={4!r}, comment="""{5}""")'.format( - self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.uncertainty, self.comment) + return 'KineticsModel(Tmin={0!r}, Tmax={1!r}, Pmin={2!r}, Pmax={3!r}, uncertainty={4!r}, solute={5!r}, comment="""{6}""")'.format( + self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.uncertainty, self.solute, self.comment) def __reduce__(self): """ A helper function used when pickling a KineticsModel object. """ - return (KineticsModel, (self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.uncertainty, self.comment)) + return (KineticsModel, (self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.uncertainty, self.solute, self.comment)) property Tmin: """The minimum temperature at which the model is valid, or ``None`` if not defined.""" @@ -196,6 +200,7 @@ cdef class KineticsModel: """ raise NotImplementedError('Unexpected call to KineticsModel.get_rate_coefficient(); ' 'you should be using a class derived from KineticsModel.') + cpdef to_html(self): """ @@ -234,6 +239,9 @@ cdef class KineticsModel: if other_kinetics.is_pressure_dependent(): return False + if isinstance(other_kinetics, (StickingCoefficient, StickingCoefficientBEP)): + return False + for T in [500, 1000, 1500, 2000]: if abs(log10(self.get_rate_coefficient(T)) - log10(other_kinetics.get_rate_coefficient(T))) > 0.5: return False @@ -403,7 +411,7 @@ cdef class PDepKineticsModel(KineticsModel): cdef double eff cdef int i - all_efficiencies = np.ones(len(species), np.float64) + all_efficiencies = np.ones(len(species), float) for mol, eff in self.efficiencies.iteritems(): for spec in species: if spec.is_isomorphic(mol): diff --git a/molecule/kinetics/surface.pxd b/molecule/kinetics/surface.pxd index e3349d5..c41a066 100644 --- a/molecule/kinetics/surface.pxd +++ b/molecule/kinetics/surface.pxd @@ -34,7 +34,7 @@ from molecule.quantity cimport ScalarQuantity, ArrayQuantity ################################################################################ cdef class StickingCoefficient(KineticsModel): - + cdef public ScalarQuantity _A cdef public ScalarQuantity _n cdef public ScalarQuantity _Ea @@ -47,8 +47,10 @@ cdef class StickingCoefficient(KineticsModel): cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=?, np.ndarray weights=?, bint three_params=?) + cpdef bint is_similar_to(self, KineticsModel other_kinetics) except -2 + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 - + cpdef change_rate(self, double factor) cpdef to_html(self) @@ -63,15 +65,70 @@ cdef class StickingCoefficientBEP(KineticsModel): cpdef double get_sticking_coefficient(self, double T, double dHrxn=?) except -1 cpdef double get_activation_energy(self, double dHrxn) except -1 cpdef StickingCoefficient to_arrhenius(self, double dHrxn) + cpdef bint is_similar_to(self, KineticsModel other_kinetics) except -2 cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 cpdef change_rate(self, double factor) ################################################################################ cdef class SurfaceArrhenius(Arrhenius): + cdef public dict _coverage_dependence - pass + cpdef SurfaceChargeTransfer to_surface_charge_transfer(self, double V0, double electrons=?) + ################################################################################ cdef class SurfaceArrheniusBEP(ArrheniusEP): cdef public dict _coverage_dependence pass +################################################################################ +cdef class SurfaceChargeTransfer(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ScalarQuantity _Ea + cdef public ScalarQuantity _T0 + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _alpha + cdef public ScalarQuantity _electrons + + cpdef double get_activation_energy_from_potential(self, double V=?, bint non_negative=?) + + cpdef double get_rate_coefficient(self, double T, double V=?) except -1 + + cpdef change_rate(self, double factor) + + cpdef change_t0(self, double T0) + + cpdef change_v0(self, double V0) + + cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=?, np.ndarray weights=?, bint three_params=?) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef SurfaceArrhenius to_surface_arrhenius(self) + + cpdef SurfaceChargeTransferBEP to_surface_charge_transfer_bep(self, double dGrxn, double V0=?) + +################################################################################ +cdef class SurfaceChargeTransferBEP(KineticsModel): + + cdef public ScalarQuantity _A + cdef public ScalarQuantity _n + cdef public ScalarQuantity _E0 + cdef public ScalarQuantity _V0 + cdef public ScalarQuantity _alpha + cdef public ScalarQuantity _electrons + + cpdef change_v0(self, double V0) + + cpdef double get_activation_energy(self, double dGrxn) except -1 + + cpdef double get_activation_energy_from_potential(self, double V, double dGrxn) except -1 + + cpdef double get_rate_coefficient_from_potential(self, double T, double V, double dGrxn) except -1 + + cpdef SurfaceChargeTransfer to_surface_charge_transfer(self, double dGrxn) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2 + + cpdef change_rate(self, double factor) diff --git a/molecule/kinetics/surface.pyx b/molecule/kinetics/surface.pyx index d52ee28..6ea0ac7 100644 --- a/molecule/kinetics/surface.pyx +++ b/molecule/kinetics/surface.pyx @@ -29,7 +29,7 @@ import numpy as np cimport numpy as np -from libc.math cimport exp, sqrt +from libc.math cimport exp, sqrt, log10 cimport molecule.constants as constants import molecule.quantity as quantity @@ -50,7 +50,7 @@ cdef class StickingCoefficient(KineticsModel): ======================= ============================================================= Attribute Description ======================= ============================================================= - `A` The preexponential factor + `A` The preexponential factor. Unitless (sticking probability) `T0` The reference temperature `n` The temperature exponent `Ea` The activation energy @@ -87,7 +87,7 @@ cdef class StickingCoefficient(KineticsModel): if self.coverage_dependence: string += ", coverage_dependence={" for species, parameters in self.coverage_dependence.items(): - string += f"{species.to_chemkin()!r}: {{'a':{parameters['a']}, 'm':{parameters['m']}, 'E':({parameters['E'].value}, '{parameters['E'].units}')}}," + string += f"{species.to_chemkin()!r}: {{'a':{repr(parameters['a'])}, 'm':{repr(parameters['m'])}, 'E':{repr(parameters['E'])}}}," string += "}" if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) @@ -173,12 +173,12 @@ cdef class StickingCoefficient(KineticsModel): if len(Tlist) < 3 + three_params: raise KineticsError('Not enough degrees of freedom to fit this Arrhenius expression') if three_params: - A = np.zeros((len(Tlist), 3), np.float64) + A = np.zeros((len(Tlist), 3), float) A[:, 0] = np.ones_like(Tlist) A[:, 1] = np.log(Tlist / T0) A[:, 2] = -1.0 / constants.R / Tlist else: - A = np.zeros((len(Tlist), 2), np.float64) + A = np.zeros((len(Tlist), 2), float) A[:, 0] = np.ones_like(Tlist) A[:, 1] = -1.0 / constants.R / Tlist b = np.log(klist) @@ -220,6 +220,22 @@ cdef class StickingCoefficient(KineticsModel): self._A.value_si /= (self._T0.value_si / T0) ** self._n.value_si self._T0.value_si = T0 + cpdef bint is_similar_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if the sticking coefficient at temperatures 500,1000,1500,2000 K + are within +/ .5 for log(k), in other words, within a factor of 3. + """ + cdef double T + + if not isinstance(other_kinetics, StickingCoefficient): + return False + + for T in [500, 1000, 1500, 2000]: + if abs(log10(self.get_sticking_coefficient(T)) - log10(other_kinetics.get_sticking_coefficient(T))) > 0.5: + return False + + return True + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: """ Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature @@ -269,6 +285,34 @@ cdef class StickingCoefficient(KineticsModel): return string + def to_cantera_kinetics(self): + """ + Converts the RMG StickingCoefficient object to a cantera StickingArrheniusRate + + StickingArrheniusRate(A,b,E) where A is dimensionless, b is dimensionless, and E is in J/kmol + """ + import cantera as ct + import molecule.quantity + if type(self._A) != molecule.quantity.ScalarQuantity: + raise TypeError("A factor must be a dimensionless ScalarQuantity") + A = self._A.value_si + b = self._n.value_si + E = self._Ea.value_si * 1000 # convert from J/mol to J/kmol + + return ct.StickingArrheniusRate(A, b, E) + + + def set_cantera_kinetics(self, ct_reaction, species_list): + """ + Passes in a cantera Reaction() object and sets its + rate to a Cantera ArrheniusRate object. + """ + import cantera as ct + assert isinstance(ct_reaction.rate, ct.StickingArrheniusRate), "Must have a Cantera StickingArrheniusRate attribute" + + # Set the rate parameter to a cantera Arrhenius object + ct_reaction.rate = self.to_cantera_kinetics() + ################################################################################ cdef class StickingCoefficientBEP(KineticsModel): """ @@ -320,7 +364,7 @@ cdef class StickingCoefficientBEP(KineticsModel): if self.coverage_dependence: string += ", coverage_dependence={" for species, parameters in self.coverage_dependence.items(): - string += f"{species.to_chemkin()!r}: {{'a':{parameters['a']}, 'm':{parameters['m']}, 'E':({parameters['E'].value}, '{parameters['E'].units}')}}," + string += f"{species.to_chemkin()!r}: {{'a':{repr(parameters['a'])}, 'm':{repr(parameters['m'])}, 'E':{repr(parameters['E'])}}}," string += "}" if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) @@ -336,7 +380,7 @@ cdef class StickingCoefficientBEP(KineticsModel): self.Pmin, self.Pmax, self.coverage_dependence, self.comment)) property A: - """The preexponential factor.""" + """The preexponential factor. Dimensionless (sticking probability)""" def __get__(self): return self._A def __set__(self, value): @@ -379,7 +423,8 @@ cdef class StickingCoefficientBEP(KineticsModel): cpdef double get_sticking_coefficient(self, double T, double dHrxn=0.0) except -1: """ Return the sticking coefficient (dimensionless) at - temperature `T` in K and enthalpy of reaction `dHrxn` in J/mol. + temperature `T` in K and enthalpy of reaction `dHrxn` in J/mol. + Never exceeds 1.0. """ cdef double A, n, Ea, stickingCoefficient Ea = self.get_activation_energy(dHrxn) @@ -422,6 +467,22 @@ cdef class StickingCoefficientBEP(KineticsModel): comment=self.comment, ) + cpdef bint is_similar_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if sticking coefficient at temperatures 500,1000,1500,2000 K + are within +/ .5 for log(k), in other words, within a factor of 3. + """ + cdef double T + + if not isinstance(other_kinetics, StickingCoefficientBEP): + return False + + for T in [500, 1000, 1500, 2000]: + if abs(log10(self.get_sticking_coefficient(T)) - log10(other_kinetics.get_sticking_coefficient(T))) > 0.5: + return False + + return True + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: """ Returns ``True`` if kinetics matches that of another kinetics model. Must match type, temperature @@ -492,7 +553,7 @@ cdef class SurfaceArrhenius(Arrhenius): property A: """The preexponential factor. - This is the only thing different from a normal Arrhenius class.""" + This (and the coverage dependence) is the only thing different from a normal Arrhenius class.""" def __get__(self): return self._A def __set__(self, value): @@ -522,7 +583,7 @@ cdef class SurfaceArrhenius(Arrhenius): if self.coverage_dependence: string += ", coverage_dependence={" for species, parameters in self.coverage_dependence.items(): - string += f"{species.to_chemkin()!r}: {{'a':{parameters['a']}, 'm':{parameters['m']}, 'E':({parameters['E'].value}, '{parameters['E'].units}')}}," + string += f"{species.to_chemkin()!r}: {{'a':{repr(parameters['a'])}, 'm':{repr(parameters['m'])}, 'E':{repr(parameters['E'])}}}," string += "}" if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) @@ -538,6 +599,75 @@ cdef class SurfaceArrhenius(Arrhenius): return (SurfaceArrhenius, (self.A, self.n, self.Ea, self.T0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, self.coverage_dependence, self.uncertainty, self.comment)) + cpdef SurfaceChargeTransfer to_surface_charge_transfer(self, double V0, double electrons=-1): + """ + Return an :class:`SurfaceChargeTransfer` instance of the kinetics model with reversible + potential `V0` in Volts and electron stochiometric coeff ` electrons` + """ + return SurfaceChargeTransfer( + A=self.A, + n=self.n, + electrons= electrons, + Ea=self.Ea, + V0=(V0,'V'), + T0=(1, "K"), + Tmin=self.Tmin, + Tmax=self.Tmax, + uncertainty = self.uncertainty, + comment=self.comment, + ) + + + def to_cantera_kinetics(self): + """ + Converts the RMG SurfaceArrhenius object to a cantera InterfaceArrheniusRate + + InterfaceArrheniusRate(A,b,E) where A is in units like m^2/kmol/s (depending on dimensionality) + b is dimensionless, and E is in J/kmol + """ + import cantera as ct + + rate_units_conversion = {'1/s': 1, + 's^-1': 1, + 'm^2/(mol*s)': 1000, + 'm^4/(mol^2*s)': 1000000, + 'cm^2/(mol*s)': 1000, + 'cm^4/(mol^2*s)': 1000000, + 'm^2/(molecule*s)': 1000, + 'm^4/(molecule^2*s)': 1000000, + 'cm^2/(molecule*s)': 1000, + 'cm^4/(molecule^2*s)': 1000000, + 'cm^5/(mol^2*s)': 1000000, + 'm^5/(mol^2*s)': 1000000, + } + + if self._T0.value_si != 1: + A = self._A.value_si / (self._T0.value_si) ** self._n.value_si + else: + A = self._A.value_si + + try: + A *= rate_units_conversion[self._A.units] # convert from /mol to /kmol + except KeyError: + raise ValueError('Arrhenius A-factor units {0} not found among accepted units for converting to ' + 'Cantera Arrhenius object.'.format(self._A.units)) + + b = self._n.value_si + E = self._Ea.value_si * 1000 # convert from J/mol to J/kmol + return ct.InterfaceArrheniusRate(A, b, E) + + def set_cantera_kinetics(self, ct_reaction, species_list): + """ + Takes in a cantera Reaction object and sets its + rate to a cantera InterfaceArrheniusRate object. + """ + import cantera as ct + if not isinstance(ct_reaction.rate, ct.InterfaceArrheniusRate): + raise TypeError("ct_reaction.rate must be an InterfaceArrheniusRate") + + # Set the rate parameter to a cantera Arrhenius object + ct_reaction.rate = self.to_cantera_kinetics() + ################################################################################ cdef class SurfaceArrheniusBEP(ArrheniusEP): @@ -616,7 +746,7 @@ cdef class SurfaceArrheniusBEP(ArrheniusEP): if self.coverage_dependence: string += ", coverage_dependence={" for species, parameters in self.coverage_dependence.items(): - string += f"{species.to_chemkin()!r}: {{'a':{parameters['a']}, 'm':{parameters['m']}, 'E':({parameters['E'].value}, '{parameters['E'].units}')}}," + string += f"{species.to_chemkin()!r}: {{'a':{repr(parameters['a'])}, 'm':{repr(parameters['m'])}, 'E':{repr(parameters['E'])}}}," string += "}" if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) @@ -652,3 +782,504 @@ cdef class SurfaceArrheniusBEP(ArrheniusEP): coverage_dependence=self.coverage_dependence, comment=self.comment, ) + +################################################################################ + +cdef class SurfaceChargeTransfer(KineticsModel): + + """ + A kinetics model for surface charge transfer reactions + + It is very similar to the :class:`SurfaceArrhenius`, but the Ea is potential-dependent + + + The attributes are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `T0` The reference temperature + `n` The temperature exponent + `Ea` The activation energy + `electrons` The stochiometry coeff for electrons (negative if reactant, positive if product) + `V0` The reference potential + `alpha` The charge transfer coefficient + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, Ea=None, V0=None, alpha=0.5, electrons=-1, T0=(1.0, "K"), Tmin=None, Tmax=None, + Pmin=None, Pmax=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, + comment=comment) + + self.alpha = alpha + self.A = A + self.n = n + self.Ea = Ea + self.T0 = T0 + self.electrons = electrons + self.V0 = V0 + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Arrhenius object. + """ + string = 'SurfaceChargeTransfer(A={0!r}, n={1!r}, Ea={2!r}, V0={3!r}, alpha={4!r}, electrons={5!r}, T0={6!r}'.format( + self.A, self.n, self.Ea, self.V0, self.alpha, self.electrons, self.T0) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a SurfaceChargeTransfer object. + """ + return (SurfaceChargeTransfer, (self.A, self.n, self.Ea, self.V0, self.alpha, self.electrons, self.T0, self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.SurfaceRateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property Ea: + """The activation energy.""" + def __get__(self): + return self._Ea + def __set__(self, value): + self._Ea = quantity.Energy(value) + + property T0: + """The reference temperature.""" + def __get__(self): + return self._T0 + def __set__(self, value): + self._T0 = quantity.Temperature(value) + + property V0: + """The reference potential.""" + def __get__(self): + return self._V0 + def __set__(self, value): + self._V0 = quantity.Potential(value) + + property electrons: + """The number of electrons transferred.""" + def __get__(self): + return self._electrons + def __set__(self, value): + self._electrons = quantity.Dimensionless(value) + + property alpha: + """The charge transfer coefficient.""" + def __get__(self): + return self._alpha + def __set__(self, value): + self._alpha = quantity.Dimensionless(value) + + cpdef double get_activation_energy_from_potential(self, double V=0.0, bint non_negative=True): + """ + Return the effective activation energy (in J/mol) at specificed potential (in Volts). + """ + cdef double electrons, alpha, Ea, V0 + + electrons = self._electrons.value_si + alpha = self._alpha.value_si + Ea = self._Ea.value_si + V0 = self._V0.value_si + + Ea -= alpha * electrons * constants.F * (V-V0) + + if non_negative is True: + if Ea < 0: + Ea = 0.0 + + return Ea + + cpdef double get_rate_coefficient(self, double T, double V=0.0) except -1: + """ + Return the rate coefficient in the appropriate combination of m^2, + mol, and s at temperature `T` in K. + """ + cdef double A, n, V0, T0, Ea + + A = self._A.value_si + n = self._n.value_si + V0 = self._V0.value_si + T0 = self._T0.value_si + + if V != V0: + Ea = self.get_activation_energy_from_potential(V) + else: + Ea = self._Ea.value_si + + return A * (T / T0) ** n * exp(-Ea / (constants.R * T)) + + cpdef change_t0(self, double T0): + """ + Changes the reference temperature used in the exponent to `T0` in K, + and adjusts the preexponential factor accordingly. + """ + self._A.value_si /= (self._T0.value_si / T0) ** self._n.value_si + self._T0.value_si = T0 + + cpdef change_v0(self, double V0): + """ + Changes the reference potential to `V0` in volts, and adjusts the + activation energy `Ea` accordingly. + """ + + self._Ea.value_si = self.get_activation_energy_from_potential(V0) + self._V0.value_si = V0 + + cpdef fit_to_data(self, np.ndarray Tlist, np.ndarray klist, str kunits, double T0=1, + np.ndarray weights=None, bint three_params=False): + """ + Fit the Arrhenius parameters to a set of rate coefficient data `klist` + in units of `kunits` corresponding to a set of temperatures `Tlist` in + K. A linear least-squares fit is used, which guarantees that the + resulting parameters provide the best possible approximation to the + data. + """ + import scipy.stats + if not all(np.isfinite(klist)): + raise ValueError("Rates must all be finite, not inf or NaN") + if any(klist<0): + if not all(klist<0): + raise ValueError("Rates must all be positive or all be negative.") + rate_sign_multiplier = -1 + klist = -1 * klist + else: + rate_sign_multiplier = 1 + + assert len(Tlist) == len(klist), "length of temperatures and rates must be the same" + if len(Tlist) < 3 + three_params: + raise KineticsError('Not enough degrees of freedom to fit this Arrhenius expression') + if three_params: + A = np.zeros((len(Tlist), 3), np.float64) + A[:, 0] = np.ones_like(Tlist) + A[:, 1] = np.log(Tlist / T0) + A[:, 2] = -1.0 / constants.R / Tlist + else: + A = np.zeros((len(Tlist), 2), np.float64) + A[:, 0] = np.ones_like(Tlist) + A[:, 1] = -1.0 / constants.R / Tlist + b = np.log(klist) + if weights is not None: + for n in range(b.size): + A[n, :] *= weights[n] + b[n] *= weights[n] + x, residues, rank, s = np.linalg.lstsq(A, b, rcond=RCOND) + + # Determine covarianace matrix to obtain parameter uncertainties + count = klist.size + cov = residues[0] / (count - 3) * np.linalg.inv(np.dot(A.T, A)) + t = scipy.stats.t.ppf(0.975, count - 3) + + if not three_params: + x = np.array([x[0], 0, x[1]]) + cov = np.array([[cov[0, 0], 0, cov[0, 1]], [0, 0, 0], [cov[1, 0], 0, cov[1, 1]]]) + + self.A = (rate_sign_multiplier * exp(x[0]), kunits) + self.n = x[1] + self.Ea = (x[2] * 0.001, "kJ/mol") + self.T0 = (T0, "K") + self.Tmin = (np.min(Tlist), "K") + self.Tmax = (np.max(Tlist), "K") + self.comment = 'Fitted to {0:d} data points; dA = *|/ {1:g}, dn = +|- {2:g}, dEa = +|- {3:g} kJ/mol'.format( + len(Tlist), + exp(sqrt(cov[0, 0])), + sqrt(cov[1, 1]), + sqrt(cov[2, 2]) * 0.001, + ) + + return self + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, SurfaceChargeTransfer): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.Ea.equals(other_kinetics.Ea) or not self.T0.equals(other_kinetics.T0) + or not self.alpha.equals(other_kinetics.alpha) or not self.electrons.equals(other_kinetics.electrons) + or not self.V0.equals(other_kinetics.V0)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor in Arrhenius expression by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + + cpdef SurfaceArrhenius to_surface_arrhenius(self): + """ + Return an :class:`SurfaceArrhenius` instance of the kinetics model + """ + return SurfaceArrhenius( + A=self.A, + n=self.n, + Ea=self.Ea, + T0=(1, "K"), + Tmin=self.Tmin, + Tmax=self.Tmax, + uncertainty = self.uncertainty, + comment=self.comment, + ) + + cpdef SurfaceChargeTransferBEP to_surface_charge_transfer_bep(self, double dGrxn, double V0=0.0): + """ + Converts an SurfaceChargeTransfer object to SurfaceChargeTransferBEP + """ + cdef double E0 + + self.change_t0(1) + self.change_v0(V0) + + E0 = self.Ea.value_si - self._alpha.value_si * dGrxn + if E0 < 0: + E0 = 0.0 + + aep = SurfaceChargeTransferBEP( + A=self.A, + electrons=self.electrons, + n=self.n, + alpha=self.alpha, + V0=self.V0, + E0=(E0, 'J/mol'), + Tmin=self.Tmin, + Tmax=self.Tmax, + Pmin=self.Pmin, + Pmax=self.Pmax, + uncertainty=self.uncertainty, + comment=self.comment) + return aep + +cdef class SurfaceChargeTransferBEP(KineticsModel): + """ + A kinetics model based on the (modified) Arrhenius equation, using the + Evans-Polanyi equation to determine the activation energy. The attributes + are: + + =============== ============================================================= + Attribute Description + =============== ============================================================= + `A` The preexponential factor + `n` The temperature exponent + `E0` The activation energy at equilibiurm + ` electrons` The stochiometry coeff for electrons (negative if reactant, positive if product) + `V0` The reference potential + `alpha` The charge transfer coefficient + `Tmin` The minimum temperature at which the model is valid, or zero if unknown or undefined + `Tmax` The maximum temperature at which the model is valid, or zero if unknown or undefined + `Pmin` The minimum pressure at which the model is valid, or zero if unknown or undefined + `Pmax` The maximum pressure at which the model is valid, or zero if unknown or undefined + `comment` Information about the model (e.g. its source) + =============== ============================================================= + + """ + + def __init__(self, A=None, n=0.0, E0=None, V0=(0.0,'V'), alpha=0.5, electrons=-1, Tmin=None, Tmax=None, + Pmin=None, Pmax=None, uncertainty=None, comment=''): + + KineticsModel.__init__(self, Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax, uncertainty=uncertainty, + comment=comment) + + self.alpha = alpha + self.A = A + self.n = n + self.E0 = E0 + self.electrons = electrons + self.V0 = V0 + + def __repr__(self): + """ + Return a string representation that can be used to reconstruct the + Arrhenius object. + """ + string = 'SurfaceChargeTransferBEP(A={0!r}, n={1!r}, E0={2!r}, V0={3!r}, alpha={4!r}, electrons={5!r}'.format( + self.A, self.n, self.E0, self.V0, self.alpha, self.electrons) + if self.Tmin is not None: string += ', Tmin={0!r}'.format(self.Tmin) + if self.Tmax is not None: string += ', Tmax={0!r}'.format(self.Tmax) + if self.Pmin is not None: string += ', Pmin={0!r}'.format(self.Pmin) + if self.Pmax is not None: string += ', Pmax={0!r}'.format(self.Pmax) + if self.uncertainty: string += ', uncertainty={0!r}'.format(self.uncertainty) + if self.comment != '': string += ', comment="""{0}"""'.format(self.comment) + string += ')' + return string + + def __reduce__(self): + """ + A helper function used when pickling a SurfaceChargeTransfer object. + """ + return (SurfaceChargeTransferBEP, (self.A, self.n, self.E0, self.V0, self.alpha, self.electrons, self.Tmin, self.Tmax, self.Pmin, self.Pmax, + self.uncertainty, self.comment)) + + property A: + """The preexponential factor.""" + def __get__(self): + return self._A + def __set__(self, value): + self._A = quantity.SurfaceRateCoefficient(value) + + property n: + """The temperature exponent.""" + def __get__(self): + return self._n + def __set__(self, value): + self._n = quantity.Dimensionless(value) + + property E0: + """The activation energy.""" + def __get__(self): + return self._E0 + def __set__(self, value): + self._E0 = quantity.Energy(value) + + property V0: + """The reference potential.""" + def __get__(self): + return self._V0 + def __set__(self, value): + self._V0 = quantity.Potential(value) + + property electrons: + """The number of electrons transferred.""" + def __get__(self): + return self._electrons + def __set__(self, value): + self._electrons = quantity.Dimensionless(value) + + property alpha: + """The charge transfer coefficient.""" + def __get__(self): + return self._alpha + def __set__(self, value): + self._alpha = quantity.Dimensionless(value) + + cpdef change_v0(self, double V0): + """ + Changes the reference potential to `V0` in volts, and adjusts the + activation energy `E0` accordingly. + """ + + self._E0.value_si = self.get_activation_energy_from_potential(V0,0.0) + self._V0.value_si = V0 + + cpdef double get_activation_energy(self, double dGrxn) except -1: + """ + Return the activation energy in J/mol corresponding to the given + free energy of reaction `dGrxn` in J/mol at the reference potential. + """ + cdef double Ea + Ea = self._alpha.value_si * dGrxn + self._E0.value_si + + if Ea < 0.0: + Ea = 0.0 + elif dGrxn > 0.0 and Ea < dGrxn: + Ea = dGrxn + + return Ea + + cpdef double get_activation_energy_from_potential(self, double V, double dGrxn) except -1: + """ + Return the activation energy in J/mol corresponding to the given + free energy of reaction `dGrxn` in J/mol. + """ + cdef double Ea + Ea = self.get_activation_energy(dGrxn) + Ea -= self._alpha.value_si * self._electrons.value_si * constants.F * (V-self._V0.value_si) + + return Ea + + cpdef double get_rate_coefficient_from_potential(self, double T, double V, double dGrxn) except -1: + """ + Return the rate coefficient in the appropriate combination of m^3, + mol, and s at temperature `T` in K, potential `V` in volts, and + free of reaction `dGrxn` in J/mol. + """ + cdef double A, n, Ea + Ea = self.get_activation_energy_from_potential(V,dGrxn) + A = self._A.value_si + n = self._n.value_si + return A * T ** n * exp(-Ea / (constants.R * T)) + + cpdef SurfaceChargeTransfer to_surface_charge_transfer(self, double dGrxn): + """ + Return an :class:`SurfaceChargeTransfer` instance of the kinetics model using the + given free energy of reaction `dGrxn` to determine the activation energy. + """ + return SurfaceChargeTransfer( + A=self.A, + n=self.n, + electrons=self.electrons, + Ea=(self.get_activation_energy(dGrxn) * 0.001, "kJ/mol"), + V0=self.V0, + T0=(1, "K"), + Tmin=self.Tmin, + Tmax=self.Tmax, + Pmin=self.Pmin, + Pmax=self.Pmax, + uncertainty=self.uncertainty, + comment=self.comment, + ) + + cpdef bint is_identical_to(self, KineticsModel other_kinetics) except -2: + """ + Returns ``True`` if kinetics matches that of another kinetics model. Must match temperature + and pressure range of kinetics model, as well as parameters: A, n, Ea, T0. (Shouldn't have pressure + range if it's Arrhenius.) Otherwise returns ``False``. + """ + if not isinstance(other_kinetics, SurfaceChargeTransferBEP): + return False + if not KineticsModel.is_identical_to(self, other_kinetics): + return False + if (not self.A.equals(other_kinetics.A) or not self.n.equals(other_kinetics.n) + or not self.E0.equals(other_kinetics.E0) or not self.alpha.equals(other_kinetics.alpha) + or not self.electrons.equals(other_kinetics.electrons) or not self.V0.equals(other_kinetics.V0)): + return False + + return True + + cpdef change_rate(self, double factor): + """ + Changes A factor by multiplying it by a ``factor``. + """ + self._A.value_si *= factor + + def set_cantera_kinetics(self, ct_reaction, species_list): + """ + Sets a cantera ElementaryReaction() object with the modified Arrhenius object + converted to an Arrhenius form. + """ + raise NotImplementedError('set_cantera_kinetics() is not implemented for ArrheniusEP class kinetics.') diff --git a/molecule/ml/estimator.py b/molecule/ml/estimator.py index 9e89a1c..20657b3 100644 --- a/molecule/ml/estimator.py +++ b/molecule/ml/estimator.py @@ -32,10 +32,10 @@ from argparse import Namespace from typing import Callable, Union +chemprop = None try: import chemprop except ImportError as e: - chemprop = None chemprop_exception = e import numpy as np @@ -43,6 +43,16 @@ from molecule.species import Species from molecule.thermo import ThermoData +ADMONITION = """ +Support for predicting thermochemistry using chemprop has been temporarily removed +from RMG, pending official chemprop support for Python 3.11 and newer. + +To use chemprop and RMG, install a previous version of RMG (3.1.1 or earlier). + +See the link below for status of re-integration of chemprop: +https://github.com/ReactionMechanismGenerator/RMG-Py/issues/2559 +""" + class MLEstimator: """ @@ -118,7 +128,7 @@ def load_estimator(model_dir: str) -> Callable[[str], np.ndarray]: if chemprop is None: # Delay chemprop ImportError until we actually try to use it # so that RMG can load successfully without chemprop. - raise chemprop_exception + raise RuntimeError(ADMONITION + "\nOriginal Exception:\n" + str(chemprop_exception)) args = Namespace() # Simple class to hold attributes diff --git a/molecule/molecule/adjlist.py b/molecule/molecule/adjlist.py index d86c02e..2ba550b 100644 --- a/molecule/molecule/adjlist.py +++ b/molecule/molecule/adjlist.py @@ -58,7 +58,8 @@ def saturate(atoms): """ new_atoms = [] for atom in atoms: - if not isinstance(atom, Atom): continue + if isinstance(atom, CuttingLabel): + continue try: max_number_of_valence_electrons = PeriodicSystem.valence_electrons[atom.symbol] except KeyError: @@ -91,7 +92,7 @@ def check_partial_charge(atom): the theoretical one: """ - if atom.symbol in ['X','Li']: + if atom.symbol in {'X','L','R','e','H+','Li'}: return # because we can't check it. valence = PeriodicSystem.valence_electrons[atom.symbol] @@ -889,12 +890,11 @@ def to_adjacency_list(atoms, multiplicity, metal='', facet='', label=None, group Convert a chemical graph defined by a list of `atoms` into a string adjacency list. """ + if old_style: + warnings.warn("Support for writing old style adjacency lists has been removed in RMG-Py v3.", RuntimeWarning) if not atoms: return '' - if old_style: - return to_old_adjacency_list(atoms, multiplicity, label, group, remove_h) - adjlist = '' # Don't remove hydrogen atoms if the molecule consists only of hydrogen atoms @@ -1139,101 +1139,3 @@ def get_old_electron_state(atom): else: raise InvalidAdjacencyListError("Cannot find electron state of atom {0}".format(atom)) return electron_state - - -def to_old_adjacency_list(atoms, multiplicity=None, label=None, group=False, remove_h=False): - """ - Convert a chemical graph defined by a list of `atoms` into a string old-style - adjacency list that can be used in RMG-Java. Currently not working for groups. - """ - warnings.warn("The old adjacency lists are no longer supported and may be" - " removed in version 2.3.", DeprecationWarning) - adjlist = '' - - if group: - raise InvalidAdjacencyListError("Not yet implemented.") - # Filter out all non-valid atoms - if not group: - for atom in atoms: - if atom.element.symbol in ['He', 'Ne', 'Ar', 'N']: - raise InvalidAdjacencyListError("Old-style adjacency list does not accept He, Ne, Ar, N elements.") - - # Don't remove hydrogen atoms if the molecule consists only of hydrogen atoms - try: - if remove_h and all([atom.element.symbol == 'H' for atom in atoms]): - remove_h = False - except AttributeError: - pass - - if label: - adjlist += label + '\n' - - # Determine the numbers to use for each atom - atom_numbers = {} - index = 0 - for atom in atoms: - if remove_h and atom.element.symbol == 'H' and atom.label == '': continue - atom_numbers[atom] = '{0:d}'.format(index + 1) - index += 1 - - atom_labels = dict([(atom, '{0}'.format(atom.label)) for atom in atom_numbers]) - - atom_types = {} - atom_electron_states = {} - if group: - raise InvalidAdjacencyListError("Not yet implemented.") - else: - for atom in atom_numbers: - # Atom type - atom_types[atom] = '{0}'.format(atom.element.symbol) - # Electron state(s) - atom_electron_states[atom] = '{0}'.format(get_old_electron_state(atom)) - - # Determine field widths - atom_number_width = max([len(s) for s in atom_numbers.values()]) + 1 - atom_label_width = max([len(s) for s in atom_labels.values()]) - if atom_label_width > 0: - atom_label_width += 1 - atom_type_width = max([len(s) for s in atom_types.values()]) + 1 - atom_electron_state_width = max([len(s) for s in atom_electron_states.values()]) - - # Assemble the adjacency list - for atom in atoms: - if atom not in atom_numbers: - continue - - # Atom number - adjlist += '{0:<{1:d}}'.format(atom_numbers[atom], atom_number_width) - # Atom label - adjlist += '{0:<{1:d}}'.format(atom_labels[atom], atom_label_width) - # Atom type(s) - adjlist += '{0:<{1:d}}'.format(atom_types[atom], atom_type_width) - # Electron state(s) - adjlist += '{0:<{1:d}}'.format(atom_electron_states[atom], atom_electron_state_width) - - # Bonds list - atoms2 = list(atom.bonds.keys()) - # sort them the same way as the atoms - atoms2.sort(key=atoms.index) - - for atom2 in atoms2: - if atom2 not in atom_numbers: - continue - - bond = atom.bonds[atom2] - adjlist += ' {{{0},'.format(atom_numbers[atom2]) - - # Bond type(s) - if group: - if len(bond.order) == 1: - adjlist += bond.get_order_str()[0] - else: - adjlist += '{{{0}}}'.format(','.join(bond.get_order_str())) - else: - adjlist += bond.get_order_str() - adjlist += '}' - - # Each atom begins on a new line - adjlist += '\n' - - return adjlist diff --git a/molecule/molecule/atomtype.pxd b/molecule/molecule/atomtype.pxd index 3aa2081..105ae31 100644 --- a/molecule/molecule/atomtype.pxd +++ b/molecule/molecule/atomtype.pxd @@ -39,6 +39,8 @@ cdef class AtomType: cdef public list decrement_radical cdef public list increment_lone_pair cdef public list decrement_lone_pair + cdef public list increment_charge + cdef public list decrement_charge cdef public list single cdef public list all_double diff --git a/molecule/molecule/atomtype.py b/molecule/molecule/atomtype.py index ed8a396..08ae6e2 100644 --- a/molecule/molecule/atomtype.py +++ b/molecule/molecule/atomtype.py @@ -30,7 +30,7 @@ """ This module defines the atom types that are available for representing molecular functional groups and substructure patterns. Each available atom type -is defined as an instance of the :class:`AtomType` class. The atom types +is defined as an instance of the :class:`AtomType` class. The atom types themselves are available in the ``ATOMTYPES`` module-level variable, or as the return value from the :meth:`get_atomtype()` method. @@ -65,6 +65,8 @@ class AtomType: `break_bond` ``list`` The atom type(s) that result when an existing single bond to this atom type is broken `increment_radical` ``list`` The atom type(s) that result when the number of radical electrons is incremented `decrement_radical` ``list`` The atom type(s) that result when the number of radical electrons is decremented + `increment_charge` ``list`` The atom type(s) that result when the number of radical electrons is decremented and charge is incremented + `decrement_charge` ``list`` The atom type(s) that result when the number of radical electrons is incremented and charge is decremented `increment_lone_pair` ``list`` The atom type(s) that result when the number of lone electron pairs is incremented `decrement_lone_pair` ``list`` The atom type(s) that result when the number of lone electron pairs is decremented @@ -106,6 +108,8 @@ def __init__(self, label='', self.break_bond = [] self.increment_radical = [] self.decrement_radical = [] + self.increment_charge = [] + self.decrement_charge = [] self.increment_lone_pair = [] self.decrement_lone_pair = [] self.single = single or [] @@ -136,6 +140,8 @@ def __reduce__(self): 'break_bond': self.break_bond, 'increment_radical': self.increment_radical, 'decrement_radical': self.decrement_radical, + 'increment_charge': self.increment_charge, + 'decrement_charge': self.decrement_charge, 'increment_lone_pair': self.increment_lone_pair, 'decrement_lone_pair': self.decrement_lone_pair, 'single': self.single, @@ -164,6 +170,8 @@ def __setstate__(self, d): self.break_bond = d['break_bond'] self.increment_radical = d['increment_radical'] self.decrement_radical = d['decrement_radical'] + self.increment_charge = d['increment_charge'] + self.decrement_charge = d['decrement_charge'] self.increment_lone_pair = d['increment_lone_pair'] self.decrement_lone_pair = d['decrement_lone_pair'] self.single = d['single'] @@ -178,7 +186,7 @@ def __setstate__(self, d): self.charge = d['charge'] def set_actions(self, increment_bond, decrement_bond, form_bond, break_bond, increment_radical, decrement_radical, - increment_lone_pair, decrement_lone_pair): + increment_lone_pair, decrement_lone_pair, increment_charge, decrement_charge): self.increment_bond = increment_bond self.decrement_bond = decrement_bond self.form_bond = form_bond @@ -187,6 +195,8 @@ def set_actions(self, increment_bond, decrement_bond, form_bond, break_bond, inc self.decrement_radical = decrement_radical self.increment_lone_pair = increment_lone_pair self.decrement_lone_pair = decrement_lone_pair + self.increment_charge = increment_charge + self.decrement_charge = decrement_charge def equivalent(self, other): """ @@ -202,7 +212,7 @@ def is_specific_case_of(self, other): atom type `atomType2` or ``False`` otherwise. """ return self is other or self in other.specific - + def get_features(self): """ Returns a list of the features that are checked to determine atomtype @@ -240,9 +250,11 @@ def get_features(self): ATOMTYPES = {} -ATOMTYPES['Rx'] = AtomType(label='Rx', generic=[], specific=[ +# Electron +ATOMTYPES['e'] = AtomType(label='e', generic=[], specific=[], lone_pairs=[0], charge=[-1]) + +ATOMTYPES['Rx'] = AtomType(label='Rx', generic=[], specific=[ 'H', - 'Li', 'R', 'R!H', 'Rx!H', @@ -260,9 +272,7 @@ def get_features(self): 'I','I1s', 'F','F1s','X','Xv','Xo']) -ATOMTYPES['Rx!H'] = AtomType(label='Rx!H', generic=[], specific=[ - 'Li', - 'R', +ATOMTYPES['Rx!H'] = AtomType(label='Rx!H', generic=['Rx'], specific=[ 'R!H', 'R!H!Val7', 'Val4','Val5','Val6','Val7', @@ -279,22 +289,22 @@ def get_features(self): 'F','F1s','X','Xv','Xo']) # Surface sites: -ATOMTYPES['X'] = AtomType(label='X', generic=['Rx', 'Rx!H'], specific=['Xv', 'Xo']) +ATOMTYPES['X'] = AtomType(label='X', generic=['Rx', 'Rx!H'], specific=['Xv', 'Xo']) # Vacant surface site: -ATOMTYPES['Xv'] = AtomType('Xv', generic=['X','Rx', 'Rx!H'], specific=[], +ATOMTYPES['Xv'] = AtomType('Xv', generic=['X','Rx', 'Rx!H'], specific=[], single=[0], all_double=[0], r_double=[], o_double=[], s_double=[], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[0]) # Occupied surface site: -ATOMTYPES['Xo'] = AtomType('Xo', generic=['X','Rx', 'Rx!H'], specific=[], +ATOMTYPES['Xo'] = AtomType('Xo', generic=['X','Rx', 'Rx!H'], specific=[], single=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], all_double=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], r_double=[], o_double=[], s_double=[], triple=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], quadruple=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], benzene=[0], lone_pairs=[0]) # Non-surface atomTypes, R being the most generic: -ATOMTYPES['R'] = AtomType(label='R', generic=['Rx'], specific=[ - 'H', - 'Li', +ATOMTYPES['R'] = AtomType(label='R', generic=['Rx'], specific=[ + 'H','H0','H+', + 'Li','Li0','Li+', 'R!H', 'R!H!Val7', 'Val4','Val5','Val6','Val7', @@ -310,9 +320,10 @@ def get_features(self): 'I','I1s', 'F','F1s']) -ATOMTYPES['R!H'] = AtomType(label='R!H', generic=['R', 'Rx', 'Rx!H'], specific=[ +ATOMTYPES['R!H'] = AtomType(label='R!H', generic=['R', 'Rx', 'Rx!H'], specific=[ 'Val4','Val5','Val6','Val7', 'He','Ne','Ar', + 'Li','Li0','Li+', 'C','Ca','Cs','Csc','Cd','CO','CS','Cdd','Cdc','Ctc','Ct','Cb','Cbf','Cq','C2s','C2sc','C2d','C2dc','C2tc', 'N','N0sc','N1s','N1sc','N1dc','N3s','N3sc','N3d','N3t','N3b','N5sc','N5dc','N5ddc','N5dddc','N5tc','N5b','N5bd', 'O','Oa','O0sc','O2s','O2sc','O2d','O4sc','O4dc','O4tc','O4b', @@ -324,9 +335,10 @@ def get_features(self): 'I','I1s', 'F','F1s']) -ATOMTYPES['R!H!Val7'] = AtomType(label='R!H!Val7', generic=['R', 'Rx', 'Rx!H'], specific=[ +ATOMTYPES['R!H!Val7'] = AtomType(label='R!H!Val7', generic=['R', 'Rx', 'Rx!H'], specific=[ 'Val4','Val5','Val6', 'He','Ne','Ar', + 'Li','Li0','Li+', 'C','Ca','Cs','Csc','Cd','CO','CS','Cdd','Cdc','Ctc','Ct','Cb','Cbf','Cq','C2s','C2sc','C2d','C2dc','C2tc', 'N','N0sc','N1s','N1sc','N1dc','N3s','N3sc','N3d','N3t','N3b','N5sc','N5dc','N5ddc','N5dddc','N5tc','N5b','N5bd', 'O','Oa','O0sc','O2s','O2sc','O2d','O4sc','O4dc','O4tc','O4b', @@ -353,15 +365,20 @@ def get_features(self): 'I','I1s', 'F','F1s']) -ATOMTYPES['Li'] = AtomType('Li', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=['Li0'], + +ATOMTYPES['H'] = AtomType('H', generic=['Rx','R'], specific=['H0','H+'], charge=[0,+1]) +ATOMTYPES['H0'] = AtomType('H0', generic=['R','H'], specific=[], single=[0,1], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], + quadruple=[0], benzene=[0], lone_pairs=[0], charge=[0]) +ATOMTYPES['H+'] = AtomType('H+', generic=['R','H'], specific=[], single=[0], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], + quadruple=[0], benzene=[0], lone_pairs=[0], charge=[+1]) + +ATOMTYPES['Li'] = AtomType('Li', generic=['R', 'R!H', 'R!H!Val7'], specific=['Li0','Li+'], single=[0,1], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[0], charge=[0,1]) -ATOMTYPES['Li0'] = AtomType('Li', generic=['Li','R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[], +ATOMTYPES['Li0'] = AtomType('Li', generic=['Li','R', 'R!H', 'R!H!Val7'], specific=[], single=[0,1], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[0], charge=[0]) -ATOMTYPES['Li+'] = AtomType('Li+', generic=['Li','R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[], +ATOMTYPES['Li+'] = AtomType('Li+', generic=['Li','R', 'R!H', 'R!H!Val7'], specific=[], single=[0], all_double=[0], r_double=[0], o_double=[0], s_double=[0], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[0], charge=[1]) -ATOMTYPES['H'] = AtomType('H', generic=['R', 'Rx'], specific=[]) - ATOMTYPES['He'] = AtomType('He', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[]) ATOMTYPES['Ne'] = AtomType('Ne', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[]) ATOMTYPES['Ar'] = AtomType('Ar', generic=['R', 'R!H', 'R!H!Val7', 'Rx', 'Rx!H'], specific=[]) @@ -677,167 +694,172 @@ def get_features(self): # examples for I1s: HI, [I], IO, CH3I, I2 ATOMTYPES['F'] = AtomType('F', generic=['R', 'R!H', 'Val7', 'Rx', 'Rx!H'], specific=['F1s']) -ATOMTYPES['F1s'] = AtomType('F1s', generic=['R', 'R!H', 'F', 'Val7', 'Rx'], specific=[], +ATOMTYPES['F1s'] = AtomType('F1s', generic=['R', 'R!H', 'F', 'Val7', 'Rx', 'Rx!H'], specific=[], single=[0,1], all_double=[0], r_double=[], o_double=[], s_double=[], triple=[0], quadruple=[0], benzene=[0], lone_pairs=[3], charge=[0]) # examples for F1s: HF, [F], FO, CH3F, F2 -ATOMTYPES['Rx'].set_actions(increment_bond=['Rx'], decrement_bond=['Rx'], form_bond=['Rx'], break_bond=['Rx'], increment_radical=['Rx'], decrement_radical=['Rx'], increment_lone_pair=['Rx'], decrement_lone_pair=['Rx']) -ATOMTYPES['Rx!H'].set_actions(increment_bond=['Rx!H'], decrement_bond=['Rx!H'], form_bond=['Rx!H'], break_bond=['Rx!H'], increment_radical=['Rx!H'], decrement_radical=['Rx!H'], increment_lone_pair=['Rx!H'], decrement_lone_pair=['Rx!H']) -ATOMTYPES['X'].set_actions(increment_bond=['X'], decrement_bond=['X'], form_bond=['X'], break_bond=['X'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Xv'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Xo'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Xo'].set_actions(increment_bond=['Xo'], decrement_bond=['Xo'], form_bond=[], break_bond=['Xv'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['R'].set_actions(increment_bond=['R'], decrement_bond=['R'], form_bond=['R'], break_bond=['R'], increment_radical=['R'], decrement_radical=['R'], increment_lone_pair=['R'], decrement_lone_pair=['R']) -ATOMTYPES['R!H'].set_actions(increment_bond=['R!H'], decrement_bond=['R!H'], form_bond=['R!H'], break_bond=['R!H'], increment_radical=['R!H'], decrement_radical=['R!H'], increment_lone_pair=['R!H'], decrement_lone_pair=['R!H']) -ATOMTYPES['R!H!Val7'].set_actions(increment_bond=['R!H!Val7'], decrement_bond=['R!H!Val7'], form_bond=['R!H!Val7'], break_bond=['R!H!Val7'], increment_radical=['R!H!Val7'], decrement_radical=['R!H!Val7'], increment_lone_pair=['R!H!Val7'], decrement_lone_pair=['R!H!Val7']) -ATOMTYPES['Val4'].set_actions(increment_bond=['Val4'], decrement_bond=['Val4'], form_bond=['Val4'], break_bond=['Val4'], increment_radical=['Val4'], decrement_radical=['Val4'], increment_lone_pair=['Val4'], decrement_lone_pair=['Val4']) -ATOMTYPES['Val5'].set_actions(increment_bond=['Val5'], decrement_bond=['Val5'], form_bond=['Val5'], break_bond=['Val5'], increment_radical=['Val5'], decrement_radical=['Val5'], increment_lone_pair=['Val5'], decrement_lone_pair=['Val5']) -ATOMTYPES['Val6'].set_actions(increment_bond=['Val6'], decrement_bond=['Val6'], form_bond=['Val6'], break_bond=['Val6'], increment_radical=['Val6'], decrement_radical=['Val6'], increment_lone_pair=['Val6'], decrement_lone_pair=['Val6']) -ATOMTYPES['Val7'].set_actions(increment_bond=['Val7'], decrement_bond=['Val7'], form_bond=['Val7'], break_bond=['Val7'], increment_radical=['Val7'], decrement_radical=['Val7'], increment_lone_pair=['Val7'], decrement_lone_pair=['Val7']) - -ATOMTYPES['H'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['H'], break_bond=['H'], increment_radical=['H'], decrement_radical=['H'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['Li'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Li'], break_bond=['Li'], increment_radical=['Li'], decrement_radical=['Li'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Li0'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Li0'], break_bond=['Li0'], increment_radical=['Li0'], decrement_radical=['Li0'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['He'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['He'], decrement_radical=['He'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Ar'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['C'].set_actions(increment_bond=['C'], decrement_bond=['C'], form_bond=['C'], break_bond=['C'], increment_radical=['C'], decrement_radical=['C'], increment_lone_pair=['C'], decrement_lone_pair=['C']) -ATOMTYPES['Ca'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['C2s']) -ATOMTYPES['Cs'].set_actions(increment_bond=['Cd', 'CO', 'CS'], decrement_bond=[], form_bond=['Cs', 'Csc'], break_bond=['Cs'], increment_radical=['Cs'], decrement_radical=['Cs'], increment_lone_pair=['C2s'], decrement_lone_pair=['C2s']) -ATOMTYPES['Csc'].set_actions(increment_bond=['Cdc'], decrement_bond=[], form_bond=['Csc'], break_bond=['Csc', 'Cs'], increment_radical=['Csc'], decrement_radical=['Csc'], increment_lone_pair=['C2sc'], decrement_lone_pair=['C2sc']) -ATOMTYPES['Cd'].set_actions(increment_bond=['Cdd', 'Ct', 'C2tc'], decrement_bond=['Cs'], form_bond=['Cd', 'Cdc'], break_bond=['Cd'], increment_radical=['Cd'], decrement_radical=['Cd'], increment_lone_pair=['C2d'], decrement_lone_pair=[]) -ATOMTYPES['Cdc'].set_actions(increment_bond=['Ctc'], decrement_bond=['Csc'], form_bond=['Cdc'], break_bond=['Cdc', 'Cd', 'CO', 'CS'], increment_radical=['Cdc'], decrement_radical=['Cdc'], increment_lone_pair=['C2dc'], decrement_lone_pair=[]) -ATOMTYPES['CO'].set_actions(increment_bond=['Cdd', 'C2tc'], decrement_bond=['Cs'], form_bond=['CO', 'Cdc'], break_bond=['CO'], increment_radical=['CO'], decrement_radical=['CO'], increment_lone_pair=['C2d'], decrement_lone_pair=[]) -ATOMTYPES['CS'].set_actions(increment_bond=['Cdd', 'C2tc'], decrement_bond=['Cs'], form_bond=['CS', 'Cdc'], break_bond=['CS'], increment_radical=['CS'], decrement_radical=['CS'], increment_lone_pair=['C2d'], decrement_lone_pair=[]) -ATOMTYPES['Cdd'].set_actions(increment_bond=[], decrement_bond=['Cd', 'CO', 'CS'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Ct'].set_actions(increment_bond=['Cq'], decrement_bond=['Cd', 'CO', 'CS'], form_bond=['Ct'], break_bond=['Ct'], increment_radical=['Ct'], decrement_radical=['Ct'], increment_lone_pair=['C2tc'], decrement_lone_pair=[]) -ATOMTYPES['Ctc'].set_actions(increment_bond=[], decrement_bond=['Cdc'], form_bond=['Ct'], break_bond=[], increment_radical=['Ctc'], decrement_radical=['Ctc'], increment_lone_pair=['C2tc'], decrement_lone_pair=[]) -ATOMTYPES['Cb'].set_actions(increment_bond=['Cbf'], decrement_bond=[], form_bond=['Cb'], break_bond=['Cb'], increment_radical=['Cb'], decrement_radical=['Cb'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Cbf'].set_actions(increment_bond=[], decrement_bond=['Cb'], form_bond=[], break_bond=['Cb'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['C2s'].set_actions(increment_bond=['C2d'], decrement_bond=[], form_bond=['C2s'], break_bond=['C2s'], increment_radical=['C2s'], decrement_radical=['C2s'], increment_lone_pair=['Ca'], decrement_lone_pair=['Cs']) -ATOMTYPES['C2sc'].set_actions(increment_bond=['C2dc'], decrement_bond=[], form_bond=['C2sc'], break_bond=['C2sc'], increment_radical=['C2sc'], decrement_radical=['C2sc'], increment_lone_pair=[], decrement_lone_pair=['Cs']) -ATOMTYPES['C2d'].set_actions(increment_bond=['C2tc'], decrement_bond=['C2s'], form_bond=['C2dc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Cd', 'CO', 'CS']) -ATOMTYPES['C2dc'].set_actions(increment_bond=[], decrement_bond=['C2sc'], form_bond=[], break_bond=['C2d'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Cdc']) -ATOMTYPES['C2tc'].set_actions(increment_bond=[], decrement_bond=['C2d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Ct','Ctc']) -ATOMTYPES['Cq'].set_actions(increment_bond=[], decrement_bond=['Ct'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['N'].set_actions(increment_bond=['N'], decrement_bond=['N'], form_bond=['N'], break_bond=['N'], increment_radical=['N'], decrement_radical=['N'], increment_lone_pair=['N'], decrement_lone_pair=['N']) -ATOMTYPES['N0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['N0sc'], break_bond=['N0sc'], increment_radical=['N0sc'], decrement_radical=['N0sc'], increment_lone_pair=[], decrement_lone_pair=['N1s', 'N1sc']) -ATOMTYPES['N1s'].set_actions(increment_bond=['N1dc'], decrement_bond=[], form_bond=['N1s'], break_bond=['N1s'], increment_radical=['N1s'], decrement_radical=['N1s'], increment_lone_pair=['N0sc'], decrement_lone_pair=['N3s', 'N3sc']) -ATOMTYPES['N1sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['N1sc'], break_bond=['N1sc'], increment_radical=['N1sc'], decrement_radical=['N1sc'], increment_lone_pair=[], decrement_lone_pair=['N3s', 'N3sc']) -ATOMTYPES['N1dc'].set_actions(increment_bond=['N1dc'], decrement_bond=['N1s', 'N1dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['N3d']) -ATOMTYPES['N3s'].set_actions(increment_bond=['N3d'], decrement_bond=[], form_bond=['N3s'], break_bond=['N3s'], increment_radical=['N3s'], decrement_radical=['N3s'], increment_lone_pair=['N1s', 'N1sc'], decrement_lone_pair=['N5sc']) -ATOMTYPES['N3sc'].set_actions(increment_bond=['N3d'], decrement_bond=[], form_bond=['N3sc'], break_bond=['N3sc'], increment_radical=['N3sc'], decrement_radical=['N3sc'], increment_lone_pair=['N1s', 'N1sc'], decrement_lone_pair=['N5sc']) -ATOMTYPES['N3d'].set_actions(increment_bond=['N3t'], decrement_bond=['N3s', 'N3sc'], form_bond=['N3d'], break_bond=['N3d'], increment_radical=['N3d'], decrement_radical=['N3d'], increment_lone_pair=['N1dc'], decrement_lone_pair=['N5dc']) -ATOMTYPES['N3t'].set_actions(increment_bond=[], decrement_bond=['N3d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N3b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5sc'].set_actions(increment_bond=['N5dc'], decrement_bond=[], form_bond=['N5sc'], break_bond=['N5sc'], increment_radical=['N5sc'], decrement_radical=['N5sc'], increment_lone_pair=['N3s', 'N3sc'], decrement_lone_pair=[]) -ATOMTYPES['N5dc'].set_actions(increment_bond=['N5ddc', 'N5tc'], decrement_bond=['N5sc'], form_bond=['N5dc'], break_bond=['N5dc'], increment_radical=['N5dc'], decrement_radical=['N5dc'], increment_lone_pair=['N3d'], decrement_lone_pair=[]) -ATOMTYPES['N5ddc'].set_actions(increment_bond=['N5dddc'], decrement_bond=['N5dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5dddc'].set_actions(increment_bond=[], decrement_bond=['N5ddc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5tc'].set_actions(increment_bond=[], decrement_bond=['N5dc'], form_bond=['N5tc'], break_bond=['N5tc'], increment_radical=['N5tc'], decrement_radical=['N5tc'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5b'].set_actions(increment_bond=['N5bd'], decrement_bond=[], form_bond=['N5b'], break_bond=['N5b'], increment_radical=['N5b'], decrement_radical=['N5b'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['N5bd'].set_actions(increment_bond=[], decrement_bond=['N5b'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['O'].set_actions(increment_bond=['O'], decrement_bond=['O'], form_bond=['O'], break_bond=['O'], increment_radical=['O'], decrement_radical=['O'], increment_lone_pair=['O'], decrement_lone_pair=['O']) -ATOMTYPES['Oa'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['O0sc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['O2s', 'O2sc']) -ATOMTYPES['O0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['O0sc'], break_bond=['Oa', 'O0sc'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['O2s', 'O2sc']) -ATOMTYPES['O2s'].set_actions(increment_bond=['O2d'], decrement_bond=[], form_bond=['O2s', 'O2sc'], break_bond=['O2s'], increment_radical=['O2s'], decrement_radical=['O2s'], increment_lone_pair=['Oa', 'O0sc'], decrement_lone_pair=['O4sc']) -ATOMTYPES['O2sc'].set_actions(increment_bond=['O2d'], decrement_bond=[], form_bond=[], break_bond=['O2s'], increment_radical=['O2sc'], decrement_radical=['O2sc'], increment_lone_pair=[], decrement_lone_pair=['O4sc']) -ATOMTYPES['O2d'].set_actions(increment_bond=[], decrement_bond=['O2s', 'O2sc'], form_bond=[], break_bond=[], increment_radical=['O2d'], decrement_radical=['O2d'], increment_lone_pair=[], decrement_lone_pair=['O4dc', 'O4tc']) -ATOMTYPES['O4sc'].set_actions(increment_bond=['O4dc'], decrement_bond=[], form_bond=['O4sc'], break_bond=['O4sc'], increment_radical=['O4sc'], decrement_radical=['O4sc'], increment_lone_pair=['O2s', 'O2sc'], decrement_lone_pair=[]) -ATOMTYPES['O4dc'].set_actions(increment_bond=['O4tc'], decrement_bond=['O4sc'], form_bond=['O4dc'], break_bond=['O4dc'], increment_radical=['O4dc'], decrement_radical=['O4dc'], increment_lone_pair=['O2d'], decrement_lone_pair=[]) -ATOMTYPES['O4tc'].set_actions(increment_bond=[], decrement_bond=['O4dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['O4b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['Si'].set_actions(increment_bond=['Si'], decrement_bond=['Si'], form_bond=['Si'], break_bond=['Si'], increment_radical=['Si'], decrement_radical=['Si'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sis'].set_actions(increment_bond=['Sid', 'SiO'], decrement_bond=[], form_bond=['Sis'], break_bond=['Sis'], increment_radical=['Sis'], decrement_radical=['Sis'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sid'].set_actions(increment_bond=['Sidd', 'Sit'], decrement_bond=['Sis'], form_bond=['Sid'], break_bond=['Sid'], increment_radical=['Sid'], decrement_radical=['Sid'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sidd'].set_actions(increment_bond=[], decrement_bond=['Sid', 'SiO'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sit'].set_actions(increment_bond=['Siq'], decrement_bond=['Sid'], form_bond=['Sit'], break_bond=['Sit'], increment_radical=['Sit'], decrement_radical=['Sit'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['SiO'].set_actions(increment_bond=['Sidd'], decrement_bond=['Sis'], form_bond=['SiO'], break_bond=['SiO'], increment_radical=['SiO'], decrement_radical=['SiO'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sib'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Sib'], break_bond=['Sib'], increment_radical=['Sib'], decrement_radical=['Sib'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Sibf'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Siq'].set_actions(increment_bond=[], decrement_bond=['Sit'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['P'].set_actions(increment_bond=['P'], decrement_bond=['P'], form_bond=['P'], break_bond=['P'], increment_radical=['P'], decrement_radical=['P'], increment_lone_pair=['P'], decrement_lone_pair=['P']) -ATOMTYPES['P0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['P0sc'], break_bond=['P0sc'], increment_radical=['P0sc'], decrement_radical=['P0sc'], increment_lone_pair=[], decrement_lone_pair=['P1s', 'P1sc']) -ATOMTYPES['P1s'].set_actions(increment_bond=['P1dc'], decrement_bond=[], form_bond=['P1s'], break_bond=['P1s'], increment_radical=['P1s'], decrement_radical=['P1s'], increment_lone_pair=['P0sc'], decrement_lone_pair=['P3s']) -ATOMTYPES['P1sc'].set_actions(increment_bond=['P1dc'], decrement_bond=[], form_bond=['P1sc'], break_bond=['P1sc'], increment_radical=['P1sc'], decrement_radical=['P1sc'], increment_lone_pair=['P0sc'], decrement_lone_pair=['P3s']) -ATOMTYPES['P1dc'].set_actions(increment_bond=[], decrement_bond=['P1s'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['P3d']) -ATOMTYPES['P3s'].set_actions(increment_bond=['P3d'], decrement_bond=[], form_bond=['P3s'], break_bond=['P3s'], increment_radical=['P3s'], decrement_radical=['P3s'], increment_lone_pair=['P1s', 'P1sc'], decrement_lone_pair=['P5s', 'P5sc']) -ATOMTYPES['P3d'].set_actions(increment_bond=['P3t'], decrement_bond=['P3s'], form_bond=['P3d'], break_bond=['P3d'], increment_radical=['P3d'], decrement_radical=['P3d'], increment_lone_pair=['P1dc'], decrement_lone_pair=['P5d', 'P5dc']) -ATOMTYPES['P3t'].set_actions(increment_bond=[], decrement_bond=['P3d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['P5t', 'P5tc']) -ATOMTYPES['P3b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5s'].set_actions(increment_bond=['P5d', 'P5dc'], decrement_bond=[], form_bond=['P5s'], break_bond=['P5s'], increment_radical=['P5s'], decrement_radical=['P5s'], increment_lone_pair=['P3s'], decrement_lone_pair=[]) -ATOMTYPES['P5sc'].set_actions(increment_bond=['P5dc'], decrement_bond=[], form_bond=['P5sc'], break_bond=['P5sc'], increment_radical=['P5sc'], decrement_radical=['P5sc'], increment_lone_pair=['P3s'], decrement_lone_pair=[]) -ATOMTYPES['P5d'].set_actions(increment_bond=['P5dd', 'P5ddc', 'P5t', 'P5tc'], decrement_bond=['P5s'], form_bond=['P5d'], break_bond=['P5d'], increment_radical=['P5d'], decrement_radical=['P5d'], increment_lone_pair=['P3d'], decrement_lone_pair=[]) -ATOMTYPES['P5dd'].set_actions(increment_bond=['P5td'], decrement_bond=['P5d', 'P5dc'], form_bond=['P5dd'], break_bond=['P5dd'], increment_radical=['P5dd'], decrement_radical=['P5dd'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5dc'].set_actions(increment_bond=['P5dd', 'P5ddc', 'P5tc'], decrement_bond=['P5sc'], form_bond=['P5dc'], break_bond=['P5dc'], increment_radical=['P5dc'], decrement_radical=['P5dc'], increment_lone_pair=['P3d'], decrement_lone_pair=[]) -ATOMTYPES['P5ddc'].set_actions(increment_bond=[], decrement_bond=['P5dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5t'].set_actions(increment_bond=['P5td'], decrement_bond=['P5d'], form_bond=['P5t'], break_bond=['P5t'], increment_radical=['P5t'], decrement_radical=['P5t'], increment_lone_pair=['P3t'], decrement_lone_pair=[]) -ATOMTYPES['P5td'].set_actions(increment_bond=[], decrement_bond=['P5t', 'P5dd'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5tc'].set_actions(increment_bond=[], decrement_bond=['P5dc'], form_bond=['P5tc'], break_bond=['P5tc'], increment_radical=['P5tc'], decrement_radical=['P5tc'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5b'].set_actions(increment_bond=['P5bd'], decrement_bond=[], form_bond=['P5b'], break_bond=['P5b'], increment_radical=['P5b'], decrement_radical=['P5b'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['P5bd'].set_actions(increment_bond=[], decrement_bond=['P5b'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['S'].set_actions(increment_bond=['S'], decrement_bond=['S'], form_bond=['S'], break_bond=['S'], increment_radical=['S'], decrement_radical=['S'], increment_lone_pair=['S'], decrement_lone_pair=['S']) -ATOMTYPES['S0sc'].set_actions(increment_bond=['S0sc'], decrement_bond=['S0sc'], form_bond=['S0sc'], break_bond=['Sa', 'S0sc'], increment_radical=['S0sc'], decrement_radical=['S0sc'], increment_lone_pair=[], decrement_lone_pair=['S2s', 'S2sc', 'S2dc', 'S2tc']) -ATOMTYPES['Sa'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['S0sc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['S2s']) -ATOMTYPES['S2s'].set_actions(increment_bond=['S2d', 'S2dc'], decrement_bond=[], form_bond=['S2s', 'S2sc'], break_bond=['S2s'], increment_radical=['S2s'], decrement_radical=['S2s'], increment_lone_pair=['Sa', 'S0sc'], decrement_lone_pair=['S4s', 'S4sc']) -ATOMTYPES['S2sc'].set_actions(increment_bond=['S2dc'], decrement_bond=[], form_bond=['S2sc'], break_bond=['S2sc', 'S2s'], increment_radical=['S2sc'], decrement_radical=['S2sc'], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4s', 'S4sc']) -ATOMTYPES['S2d'].set_actions(increment_bond=['S2tc'], decrement_bond=['S2s'], form_bond=['S2d'], break_bond=['S2d'], increment_radical=['S2d'], decrement_radical=['S2d'], increment_lone_pair=[], decrement_lone_pair=['S4dc', 'S4d']) -ATOMTYPES['S2dc'].set_actions(increment_bond=['S2tc', 'S2dc'], decrement_bond=['S2sc', 'S2s', 'S2dc'], form_bond=['S2dc'], break_bond=['S2dc'], increment_radical=['S2dc'], decrement_radical=['S2dc'], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4d', 'S4dc']) -ATOMTYPES['S2tc'].set_actions(increment_bond=[], decrement_bond=['S2d', 'S2dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4t']) -ATOMTYPES['S4s'].set_actions(increment_bond=['S4d', 'S4dc'], decrement_bond=[], form_bond=['S4s'], break_bond=['S4s'], increment_radical=['S4s'], decrement_radical=['S4s'], increment_lone_pair=['S2s', 'S2sc'], decrement_lone_pair=['S6s']) -ATOMTYPES['S4sc'].set_actions(increment_bond=['S4d', 'S4dc'], decrement_bond=[], form_bond=['S4s', 'S4sc'], break_bond=['S4sc'], increment_radical=['S4sc'], decrement_radical=['S4sc'], increment_lone_pair=['S2s', 'S2sc'], decrement_lone_pair=['S6s']) -ATOMTYPES['S4d'].set_actions(increment_bond=['S4dd', 'S4dc', 'S4t', 'S4tdc'], decrement_bond=['S4s', 'S4sc'], form_bond=['S4dc', 'S4d'], break_bond=['S4d', 'S4dc'], increment_radical=['S4d'], decrement_radical=['S4d'], increment_lone_pair=['S2d', 'S2dc'], decrement_lone_pair=['S6d', 'S6dc']) -ATOMTYPES['S4dc'].set_actions(increment_bond=['S4dd', 'S4dc', 'S4tdc'], decrement_bond=['S4sc', 'S4dc'], form_bond=['S4d', 'S4dc'], break_bond=['S4d', 'S4dc'], increment_radical=['S4dc'], decrement_radical=['S4dc'], increment_lone_pair=['S2d', 'S2dc'], decrement_lone_pair=['S6d', 'S6dc']) -ATOMTYPES['S4b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['S4dd'].set_actions(increment_bond=['S4dc'], decrement_bond=['S4dc', 'S4d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['S6dd']) -ATOMTYPES['S4t'].set_actions(increment_bond=[], decrement_bond=['S4d'], form_bond=['S4t'], break_bond=['S4t'], increment_radical=['S4t'], decrement_radical=['S4t'], increment_lone_pair=['S2tc'], decrement_lone_pair=['S6t', 'S6tdc']) -ATOMTYPES['S4tdc'].set_actions(increment_bond=['S4tdc'], decrement_bond=['S4d', 'S4tdc'], form_bond=['S4tdc'], break_bond=['S4tdc'], increment_radical=['S4tdc'], decrement_radical=['S4tdc'], increment_lone_pair=['S6tdc'], decrement_lone_pair=['S6td', 'S6tdc']) -ATOMTYPES['S6s'].set_actions(increment_bond=['S6d', 'S6dc'], decrement_bond=[], form_bond=['S6s'], break_bond=['S6s'], increment_radical=['S6s'], decrement_radical=['S6s'], increment_lone_pair=['S4s', 'S4sc'], decrement_lone_pair=[]) -ATOMTYPES['S6sc'].set_actions(increment_bond=['S6dc'], decrement_bond=[], form_bond=['S6sc'], break_bond=['S6sc'], increment_radical=['S6sc'], decrement_radical=['S6sc'], increment_lone_pair=['S4s', 'S4sc'], decrement_lone_pair=[]) -ATOMTYPES['S6d'].set_actions(increment_bond=['S6dd', 'S6t', 'S6tdc'], decrement_bond=['S6s'], form_bond=['S6d', 'S6dc'], break_bond=['S6d', 'S6dc'], increment_radical=['S6d'], decrement_radical=['S6d'], increment_lone_pair=['S4d', 'S4dc'], decrement_lone_pair=[]) -ATOMTYPES['S6dc'].set_actions(increment_bond=['S6dd', 'S6ddd', 'S6dc', 'S6t', 'S6td', 'S6tdc'], decrement_bond=['S6sc', 'S6dc'], form_bond=['S6d', 'S6dc'], break_bond=['S6d', 'S6dc'], increment_radical=['S6dc'], decrement_radical=['S6dc'], increment_lone_pair=['S4d', 'S4dc'], decrement_lone_pair=[]) -ATOMTYPES['S6dd'].set_actions(increment_bond=['S6ddd', 'S6td'], decrement_bond=['S6d', 'S6dc'], form_bond=['S6dd', 'S6dc'], break_bond=['S6dd'], increment_radical=['S6dd'], decrement_radical=['S6dd'], increment_lone_pair=['S4dd'], decrement_lone_pair=[]) -ATOMTYPES['S6ddd'].set_actions(increment_bond=[], decrement_bond=['S6dd', 'S6dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['S6t'].set_actions(increment_bond=['S6td'], decrement_bond=['S6d', 'S6dc'], form_bond=['S6t'], break_bond=['S6t'], increment_radical=['S6t'], decrement_radical=['S6t'], increment_lone_pair=['S4t'], decrement_lone_pair=[]) -ATOMTYPES['S6td'].set_actions(increment_bond=['S6tt', 'S6tdc'], decrement_bond=['S6dc', 'S6t', 'S6dd', 'S6tdc'], form_bond=['S6td'], break_bond=['S6td'], increment_radical=['S6td'], decrement_radical=['S6td'], increment_lone_pair=['S4tdc'], decrement_lone_pair=[]) -ATOMTYPES['S6tt'].set_actions(increment_bond=[], decrement_bond=['S6td', 'S6tdc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['S6tdc'].set_actions(increment_bond=['S6td', 'S6tdc', 'S6tt'], decrement_bond=['S6dc', 'S6tdc'], form_bond=['S6tdc'], break_bond=['S6tdc'], increment_radical=['S6tdc'], decrement_radical=['S6tdc'], increment_lone_pair=['S4t', 'S4tdc'], decrement_lone_pair=[]) - -ATOMTYPES['Cl'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Cl'], break_bond=['Cl'], increment_radical=['Cl'], decrement_radical=['Cl'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Cl1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Cl1s'], break_bond=['Cl1s'], increment_radical=['Cl1s'], decrement_radical=['Cl1s'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['Br'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Br'], break_bond=['Br'], increment_radical=['Br'], decrement_radical=['Br'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['Br1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Br1s'], break_bond=['Br1s'], increment_radical=['Br1s'], decrement_radical=['Br1s'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['I'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['I'], break_bond=['I'], increment_radical=['I'], decrement_radical=['I'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['I1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['I1s'], break_bond=['I1s'], increment_radical=['I1s'], decrement_radical=['I1s'], increment_lone_pair=[], decrement_lone_pair=[]) - -ATOMTYPES['F'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F'], break_bond=['F'], increment_radical=['F'], decrement_radical=['F'], increment_lone_pair=[], decrement_lone_pair=[]) -ATOMTYPES['F1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F1s'], break_bond=['F1s'], increment_radical=['F1s'], decrement_radical=['F1s'], increment_lone_pair=[], decrement_lone_pair=[]) + +ATOMTYPES['Rx'].set_actions(increment_bond=['Rx'], decrement_bond=['Rx'], form_bond=['Rx'], break_bond=['Rx'], increment_radical=['Rx'], decrement_radical=['Rx'], increment_lone_pair=['Rx'], decrement_lone_pair=['Rx'], increment_charge=['Rx'], decrement_charge=['Rx']) +ATOMTYPES['Rx!H'].set_actions(increment_bond=['Rx!H'], decrement_bond=['Rx!H'], form_bond=['Rx!H'], break_bond=['Rx!H'], increment_radical=['Rx!H'], decrement_radical=['Rx!H'], increment_lone_pair=['Rx!H'], decrement_lone_pair=['Rx!H'], increment_charge=['Rx!H'], decrement_charge=['Rx!H']) + +ATOMTYPES['e'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['X'].set_actions(increment_bond=['X'], decrement_bond=['X'], form_bond=['X'], break_bond=['X'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Xv'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Xo'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Xo'].set_actions(increment_bond=['Xo'], decrement_bond=['Xo'], form_bond=[], break_bond=['Xv'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['R'].set_actions(increment_bond=['R'], decrement_bond=['R'], form_bond=['R'], break_bond=['R'], increment_radical=['R'], decrement_radical=['R'], increment_lone_pair=['R'], decrement_lone_pair=['R'], increment_charge=['R'], decrement_charge=['R']) +ATOMTYPES['R!H'].set_actions(increment_bond=['R!H'], decrement_bond=['R!H'], form_bond=['R!H'], break_bond=['R!H'], increment_radical=['R!H'], decrement_radical=['R!H'], increment_lone_pair=['R!H'], decrement_lone_pair=['R!H'], increment_charge=['R!H'], decrement_charge=['R!H']) +ATOMTYPES['R!H!Val7'].set_actions(increment_bond=['R!H!Val7'], decrement_bond=['R!H!Val7'], form_bond=['R!H!Val7'], break_bond=['R!H!Val7'], increment_radical=['R!H!Val7'], decrement_radical=['R!H!Val7'], increment_lone_pair=['R!H!Val7'], decrement_lone_pair=['R!H!Val7'], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Val4'].set_actions(increment_bond=['Val4'], decrement_bond=['Val4'], form_bond=['Val4'], break_bond=['Val4'], increment_radical=['Val4'], decrement_radical=['Val4'], increment_lone_pair=['Val4'], decrement_lone_pair=['Val4'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Val5'].set_actions(increment_bond=['Val5'], decrement_bond=['Val5'], form_bond=['Val5'], break_bond=['Val5'], increment_radical=['Val5'], decrement_radical=['Val5'], increment_lone_pair=['Val5'], decrement_lone_pair=['Val5'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Val6'].set_actions(increment_bond=['Val6'], decrement_bond=['Val6'], form_bond=['Val6'], break_bond=['Val6'], increment_radical=['Val6'], decrement_radical=['Val6'], increment_lone_pair=['Val6'], decrement_lone_pair=['Val6'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Val7'].set_actions(increment_bond=['Val7'], decrement_bond=['Val7'], form_bond=['Val7'], break_bond=['Val7'], increment_radical=['Val7'], decrement_radical=['Val7'], increment_lone_pair=['Val7'], decrement_lone_pair=['Val7'],increment_charge=[], decrement_charge=[]) + +ATOMTYPES['H'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['H'], break_bond=['H'], increment_radical=['H'], decrement_radical=['H'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['H'], decrement_charge=['H']) +ATOMTYPES['H0'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['H0'], break_bond=['H0'], increment_radical=['H0'], decrement_radical=['H0'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['H+'], decrement_charge=[]) +ATOMTYPES['H+'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=['H0']) + +ATOMTYPES['Li'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Li'], break_bond=['Li'], increment_radical=['Li'], decrement_radical=['Li'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['Li'], decrement_charge=['Li']) +ATOMTYPES['Li0'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Li0'], break_bond=['Li0'], increment_radical=['Li0'], decrement_radical=['H0'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=['Li+'], decrement_charge=[]) +ATOMTYPES['Li+'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=['Li0']) + +ATOMTYPES['He'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['He'], decrement_radical=['He'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Ar'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['C'].set_actions(increment_bond=['C'], decrement_bond=['C'], form_bond=['C'], break_bond=['C'], increment_radical=['C'], decrement_radical=['C'], increment_lone_pair=['C'], decrement_lone_pair=['C'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Ca'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['C2s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cs'].set_actions(increment_bond=['Cd', 'CO', 'CS'], decrement_bond=[], form_bond=['Cs', 'Csc'], break_bond=['Cs'], increment_radical=['Cs'], decrement_radical=['Cs'], increment_lone_pair=['C2s'], decrement_lone_pair=['C2s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Csc'].set_actions(increment_bond=['Cdc'], decrement_bond=[], form_bond=['Csc'], break_bond=['Csc', 'Cs'], increment_radical=['Csc'], decrement_radical=['Csc'], increment_lone_pair=['C2sc'], decrement_lone_pair=['C2sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cd'].set_actions(increment_bond=['Cdd', 'Ct', 'C2tc'], decrement_bond=['Cs'], form_bond=['Cd', 'Cdc'], break_bond=['Cd'], increment_radical=['Cd'], decrement_radical=['Cd'], increment_lone_pair=['C2d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cdc'].set_actions(increment_bond=[], decrement_bond=['Csc'], form_bond=['Cdc'], break_bond=['Cdc', 'Cd', 'CO', 'CS'], increment_radical=['Cdc'], decrement_radical=['Cdc'], increment_lone_pair=['C2dc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['CO'].set_actions(increment_bond=['Cdd', 'C2tc'], decrement_bond=['Cs'], form_bond=['CO', 'Cdc'], break_bond=['CO'], increment_radical=['CO'], decrement_radical=['CO'], increment_lone_pair=['C2d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['CS'].set_actions(increment_bond=['Cdd', 'C2tc'], decrement_bond=['Cs'], form_bond=['CS', 'Cdc'], break_bond=['CS'], increment_radical=['CS'], decrement_radical=['CS'], increment_lone_pair=['C2d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cdd'].set_actions(increment_bond=[], decrement_bond=['Cd', 'CO', 'CS'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Ct'].set_actions(increment_bond=['Cq'], decrement_bond=['Cd', 'CO', 'CS'], form_bond=['Ct'], break_bond=['Ct'], increment_radical=['Ct'], decrement_radical=['Ct'], increment_lone_pair=['C2tc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cb'].set_actions(increment_bond=['Cbf'], decrement_bond=[], form_bond=['Cb'], break_bond=['Cb'], increment_radical=['Cb'], decrement_radical=['Cb'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cbf'].set_actions(increment_bond=[], decrement_bond=['Cb'], form_bond=[], break_bond=['Cb'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2s'].set_actions(increment_bond=['C2d'], decrement_bond=[], form_bond=['C2s'], break_bond=['C2s'], increment_radical=['C2s'], decrement_radical=['C2s'], increment_lone_pair=['Ca'], decrement_lone_pair=['Cs'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2sc'].set_actions(increment_bond=['C2dc'], decrement_bond=[], form_bond=['C2sc'], break_bond=['C2sc'], increment_radical=['C2sc'], decrement_radical=['C2sc'], increment_lone_pair=[], decrement_lone_pair=['Cs'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2d'].set_actions(increment_bond=['C2tc'], decrement_bond=['C2s'], form_bond=['C2dc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Cd', 'CO', 'CS'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2dc'].set_actions(increment_bond=[], decrement_bond=['C2sc'], form_bond=[], break_bond=['C2d'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Cdc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['C2tc'].set_actions(increment_bond=[], decrement_bond=['C2d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['Ct'], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cq'].set_actions(increment_bond=[], decrement_bond=['Ct'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['N'].set_actions(increment_bond=['N'], decrement_bond=['N'], form_bond=['N'], break_bond=['N'], increment_radical=['N'], decrement_radical=['N'], increment_lone_pair=['N'], decrement_lone_pair=['N'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['N0sc'], break_bond=['N0sc'], increment_radical=['N0sc'], decrement_radical=['N0sc'], increment_lone_pair=[], decrement_lone_pair=['N1s', 'N1sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N1s'].set_actions(increment_bond=['N1dc'], decrement_bond=[], form_bond=['N1s'], break_bond=['N1s'], increment_radical=['N1s'], decrement_radical=['N1s'], increment_lone_pair=['N0sc'], decrement_lone_pair=['N3s', 'N3sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N1sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['N1sc'], break_bond=['N1sc'], increment_radical=['N1sc'], decrement_radical=['N1sc'], increment_lone_pair=[], decrement_lone_pair=['N3s', 'N3sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N1dc'].set_actions(increment_bond=['N1dc'], decrement_bond=['N1s', 'N1dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['N3d'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3s'].set_actions(increment_bond=['N3d'], decrement_bond=[], form_bond=['N3s'], break_bond=['N3s'], increment_radical=['N3s'], decrement_radical=['N3s'], increment_lone_pair=['N1s', 'N1sc'], decrement_lone_pair=['N5sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3sc'].set_actions(increment_bond=['N3d'], decrement_bond=[], form_bond=['N3sc'], break_bond=['N3sc'], increment_radical=['N3sc'], decrement_radical=['N3sc'], increment_lone_pair=['N1s', 'N1sc'], decrement_lone_pair=['N5sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3d'].set_actions(increment_bond=['N3t'], decrement_bond=['N3s', 'N3sc'], form_bond=['N3d'], break_bond=['N3d'], increment_radical=['N3d'], decrement_radical=['N3d'], increment_lone_pair=['N1dc'], decrement_lone_pair=['N5dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3t'].set_actions(increment_bond=[], decrement_bond=['N3d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N3b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5sc'].set_actions(increment_bond=['N5dc'], decrement_bond=[], form_bond=['N5sc'], break_bond=['N5sc'], increment_radical=['N5sc'], decrement_radical=['N5sc'], increment_lone_pair=['N3s', 'N3sc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5dc'].set_actions(increment_bond=['N5ddc', 'N5tc'], decrement_bond=['N5sc'], form_bond=['N5dc'], break_bond=['N5dc'], increment_radical=['N5dc'], decrement_radical=['N5dc'], increment_lone_pair=['N3d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5ddc'].set_actions(increment_bond=['N5dddc'], decrement_bond=['N5dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5dddc'].set_actions(increment_bond=[], decrement_bond=['N5ddc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5tc'].set_actions(increment_bond=[], decrement_bond=['N5dc'], form_bond=['N5tc'], break_bond=['N5tc'], increment_radical=['N5tc'], decrement_radical=['N5tc'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5b'].set_actions(increment_bond=['N5bd'], decrement_bond=[], form_bond=['N5b'], break_bond=['N5b'], increment_radical=['N5b'], decrement_radical=['N5b'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['N5bd'].set_actions(increment_bond=[], decrement_bond=['N5b'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['O'].set_actions(increment_bond=['O'], decrement_bond=['O'], form_bond=['O'], break_bond=['O'], increment_radical=['O'], decrement_radical=['O'], increment_lone_pair=['O'], decrement_lone_pair=['O'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Oa'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['O0sc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['O2s', 'O2sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['O0sc'], break_bond=['Oa', 'O0sc'], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['O2s', 'O2sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O2s'].set_actions(increment_bond=['O2d'], decrement_bond=[], form_bond=['O2s', 'O2sc'], break_bond=['O2s'], increment_radical=['O2s'], decrement_radical=['O2s'], increment_lone_pair=['Oa', 'O0sc'], decrement_lone_pair=['O4sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O2sc'].set_actions(increment_bond=['O2d'], decrement_bond=[], form_bond=[], break_bond=['O2s'], increment_radical=['O2sc'], decrement_radical=['O2sc'], increment_lone_pair=[], decrement_lone_pair=['O4sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O2d'].set_actions(increment_bond=[], decrement_bond=['O2s', 'O2sc'], form_bond=[], break_bond=[], increment_radical=['O2d'], decrement_radical=['O2d'], increment_lone_pair=[], decrement_lone_pair=['O4dc', 'O4tc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['O4sc'].set_actions(increment_bond=['O4dc'], decrement_bond=[], form_bond=['O4sc'], break_bond=['O4sc'], increment_radical=['O4sc'], decrement_radical=['O4sc'], increment_lone_pair=['O2s', 'O2sc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['O4dc'].set_actions(increment_bond=['O4tc'], decrement_bond=['O4sc'], form_bond=['O4dc'], break_bond=['O4dc'], increment_radical=['O4dc'], decrement_radical=['O4dc'], increment_lone_pair=['O2d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['O4tc'].set_actions(increment_bond=[], decrement_bond=['O4dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['O4b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['Ne'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=['Ne'], decrement_radical=['Ne'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['Si'].set_actions(increment_bond=['Si'], decrement_bond=['Si'], form_bond=['Si'], break_bond=['Si'], increment_radical=['Si'], decrement_radical=['Si'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sis'].set_actions(increment_bond=['Sid', 'SiO'], decrement_bond=[], form_bond=['Sis'], break_bond=['Sis'], increment_radical=['Sis'], decrement_radical=['Sis'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sid'].set_actions(increment_bond=['Sidd', 'Sit'], decrement_bond=['Sis'], form_bond=['Sid'], break_bond=['Sid'], increment_radical=['Sid'], decrement_radical=['Sid'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sidd'].set_actions(increment_bond=[], decrement_bond=['Sid', 'SiO'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sit'].set_actions(increment_bond=['Siq'], decrement_bond=['Sid'], form_bond=['Sit'], break_bond=['Sit'], increment_radical=['Sit'], decrement_radical=['Sit'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['SiO'].set_actions(increment_bond=['Sidd'], decrement_bond=['Sis'], form_bond=['SiO'], break_bond=['SiO'], increment_radical=['SiO'], decrement_radical=['SiO'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sib'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Sib'], break_bond=['Sib'], increment_radical=['Sib'], decrement_radical=['Sib'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sibf'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Siq'].set_actions(increment_bond=[], decrement_bond=['Sit'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['P'].set_actions(increment_bond=['P'], decrement_bond=['P'], form_bond=['P'], break_bond=['P'], increment_radical=['P'], decrement_radical=['P'], increment_lone_pair=['P'], decrement_lone_pair=['P'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P0sc'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['P0sc'], break_bond=['P0sc'], increment_radical=['P0sc'], decrement_radical=['P0sc'], increment_lone_pair=[], decrement_lone_pair=['P1s', 'P1sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P1s'].set_actions(increment_bond=['P1dc'], decrement_bond=[], form_bond=['P1s'], break_bond=['P1s'], increment_radical=['P1s'], decrement_radical=['P1s'], increment_lone_pair=['P0sc'], decrement_lone_pair=['P3s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P1sc'].set_actions(increment_bond=['P1dc'], decrement_bond=[], form_bond=['P1sc'], break_bond=['P1sc'], increment_radical=['P1sc'], decrement_radical=['P1sc'], increment_lone_pair=['P0sc'], decrement_lone_pair=['P3s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P1dc'].set_actions(increment_bond=[], decrement_bond=['P1s'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['P3d'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P3s'].set_actions(increment_bond=['P3d'], decrement_bond=[], form_bond=['P3s'], break_bond=['P3s'], increment_radical=['P3s'], decrement_radical=['P3s'], increment_lone_pair=['P1s', 'P1sc'], decrement_lone_pair=['P5s', 'P5sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P3d'].set_actions(increment_bond=['P3t'], decrement_bond=['P3s'], form_bond=['P3d'], break_bond=['P3d'], increment_radical=['P3d'], decrement_radical=['P3d'], increment_lone_pair=['P1dc'], decrement_lone_pair=['P5d', 'P5dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P3t'].set_actions(increment_bond=[], decrement_bond=['P3d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['P5t', 'P5tc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['P3b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5s'].set_actions(increment_bond=['P5d', 'P5dc'], decrement_bond=[], form_bond=['P5s'], break_bond=['P5s'], increment_radical=['P5s'], decrement_radical=['P5s'], increment_lone_pair=['P3s'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5sc'].set_actions(increment_bond=['P5dc'], decrement_bond=[], form_bond=['P5sc'], break_bond=['P5sc'], increment_radical=['P5sc'], decrement_radical=['P5sc'], increment_lone_pair=['P3s'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5d'].set_actions(increment_bond=['P5dd', 'P5ddc', 'P5t', 'P5tc'], decrement_bond=['P5s'], form_bond=['P5d'], break_bond=['P5d'], increment_radical=['P5d'], decrement_radical=['P5d'], increment_lone_pair=['P3d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5dd'].set_actions(increment_bond=['P5td'], decrement_bond=['P5d', 'P5dc'], form_bond=['P5dd'], break_bond=['P5dd'], increment_radical=['P5dd'], decrement_radical=['P5dd'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5dc'].set_actions(increment_bond=['P5dd', 'P5ddc', 'P5tc'], decrement_bond=['P5sc'], form_bond=['P5dc'], break_bond=['P5dc'], increment_radical=['P5dc'], decrement_radical=['P5dc'], increment_lone_pair=['P3d'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5ddc'].set_actions(increment_bond=[], decrement_bond=['P5dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5t'].set_actions(increment_bond=['P5td'], decrement_bond=['P5d'], form_bond=['P5t'], break_bond=['P5t'], increment_radical=['P5t'], decrement_radical=['P5t'], increment_lone_pair=['P3t'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5td'].set_actions(increment_bond=[], decrement_bond=['P5t', 'P5dd'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5tc'].set_actions(increment_bond=[], decrement_bond=['P5dc'], form_bond=['P5tc'], break_bond=['P5tc'], increment_radical=['P5tc'], decrement_radical=['P5tc'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5b'].set_actions(increment_bond=['P5bd'], decrement_bond=[], form_bond=['P5b'], break_bond=['P5b'], increment_radical=['P5b'], decrement_radical=['P5b'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['P5bd'].set_actions(increment_bond=[], decrement_bond=['P5b'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['S'].set_actions(increment_bond=['S'], decrement_bond=['S'], form_bond=['S'], break_bond=['S'], increment_radical=['S'], decrement_radical=['S'], increment_lone_pair=['S'], decrement_lone_pair=['S'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S0sc'].set_actions(increment_bond=['S0sc'], decrement_bond=['S0sc'], form_bond=['S0sc'], break_bond=['Sa', 'S0sc'], increment_radical=['S0sc'], decrement_radical=['S0sc'], increment_lone_pair=[], decrement_lone_pair=['S2s', 'S2sc', 'S2dc', 'S2tc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['Sa'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['S0sc'], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['S2s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2s'].set_actions(increment_bond=['S2d', 'S2dc'], decrement_bond=[], form_bond=['S2s', 'S2sc'], break_bond=['S2s'], increment_radical=['S2s'], decrement_radical=['S2s'], increment_lone_pair=['Sa', 'S0sc'], decrement_lone_pair=['S4s', 'S4sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2sc'].set_actions(increment_bond=['S2dc'], decrement_bond=[], form_bond=['S2sc'], break_bond=['S2sc', 'S2s'], increment_radical=['S2sc'], decrement_radical=['S2sc'], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4s', 'S4sc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2d'].set_actions(increment_bond=['S2tc'], decrement_bond=['S2s'], form_bond=['S2d'], break_bond=['S2d'], increment_radical=['S2d'], decrement_radical=['S2d'], increment_lone_pair=[], decrement_lone_pair=['S4dc', 'S4d'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2dc'].set_actions(increment_bond=['S2tc', 'S2dc'], decrement_bond=['S2sc', 'S2s', 'S2dc'], form_bond=['S2dc'], break_bond=['S2dc'], increment_radical=['S2dc'], decrement_radical=['S2dc'], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4d', 'S4dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S2tc'].set_actions(increment_bond=[], decrement_bond=['S2d', 'S2dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=['S0sc'], decrement_lone_pair=['S4t'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4s'].set_actions(increment_bond=['S4d', 'S4dc'], decrement_bond=[], form_bond=['S4s'], break_bond=['S4s'], increment_radical=['S4s'], decrement_radical=['S4s'], increment_lone_pair=['S2s', 'S2sc'], decrement_lone_pair=['S6s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4sc'].set_actions(increment_bond=['S4d', 'S4dc'], decrement_bond=[], form_bond=['S4s', 'S4sc'], break_bond=['S4sc'], increment_radical=['S4sc'], decrement_radical=['S4sc'], increment_lone_pair=['S2s', 'S2sc'], decrement_lone_pair=['S6s'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4d'].set_actions(increment_bond=['S4dd', 'S4dc', 'S4t', 'S4tdc'], decrement_bond=['S4s', 'S4sc'], form_bond=['S4dc', 'S4d'], break_bond=['S4d', 'S4dc'], increment_radical=['S4d'], decrement_radical=['S4d'], increment_lone_pair=['S2d', 'S2dc'], decrement_lone_pair=['S6d', 'S6dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4dc'].set_actions(increment_bond=['S4dd', 'S4dc', 'S4tdc'], decrement_bond=['S4sc', 'S4dc'], form_bond=['S4d', 'S4dc'], break_bond=['S4d', 'S4dc'], increment_radical=['S4dc'], decrement_radical=['S4dc'], increment_lone_pair=['S2d', 'S2dc'], decrement_lone_pair=['S6d', 'S6dc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4b'].set_actions(increment_bond=[], decrement_bond=[], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4dd'].set_actions(increment_bond=['S4dc'], decrement_bond=['S4dc', 'S4d'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=['S6dd'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4t'].set_actions(increment_bond=[], decrement_bond=['S4d'], form_bond=['S4t'], break_bond=['S4t'], increment_radical=['S4t'], decrement_radical=['S4t'], increment_lone_pair=['S2tc'], decrement_lone_pair=['S6t', 'S6tdc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S4tdc'].set_actions(increment_bond=['S4tdc'], decrement_bond=['S4d', 'S4tdc'], form_bond=['S4tdc'], break_bond=['S4tdc'], increment_radical=['S4tdc'], decrement_radical=['S4tdc'], increment_lone_pair=['S6tdc'], decrement_lone_pair=['S6td', 'S6tdc'],increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6s'].set_actions(increment_bond=['S6d', 'S6dc'], decrement_bond=[], form_bond=['S6s'], break_bond=['S6s'], increment_radical=['S6s'], decrement_radical=['S6s'], increment_lone_pair=['S4s', 'S4sc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6sc'].set_actions(increment_bond=['S6dc'], decrement_bond=[], form_bond=['S6sc'], break_bond=['S6sc'], increment_radical=['S6sc'], decrement_radical=['S6sc'], increment_lone_pair=['S4s', 'S4sc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6d'].set_actions(increment_bond=['S6dd', 'S6t', 'S6tdc'], decrement_bond=['S6s'], form_bond=['S6d', 'S6dc'], break_bond=['S6d', 'S6dc'], increment_radical=['S6d'], decrement_radical=['S6d'], increment_lone_pair=['S4d', 'S4dc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6dc'].set_actions(increment_bond=['S6dd', 'S6ddd', 'S6dc', 'S6t', 'S6td', 'S6tdc'], decrement_bond=['S6sc', 'S6dc'], form_bond=['S6d', 'S6dc'], break_bond=['S6d', 'S6dc'], increment_radical=['S6dc'], decrement_radical=['S6dc'], increment_lone_pair=['S4d', 'S4dc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6dd'].set_actions(increment_bond=['S6ddd', 'S6td'], decrement_bond=['S6d', 'S6dc'], form_bond=['S6dd', 'S6dc'], break_bond=['S6dd'], increment_radical=['S6dd'], decrement_radical=['S6dd'], increment_lone_pair=['S4dd'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6ddd'].set_actions(increment_bond=[], decrement_bond=['S6dd', 'S6dc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6t'].set_actions(increment_bond=['S6td'], decrement_bond=['S6d', 'S6dc'], form_bond=['S6t'], break_bond=['S6t'], increment_radical=['S6t'], decrement_radical=['S6t'], increment_lone_pair=['S4t'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6td'].set_actions(increment_bond=['S6tt', 'S6tdc'], decrement_bond=['S6dc', 'S6t', 'S6dd', 'S6tdc'], form_bond=['S6td'], break_bond=['S6td'], increment_radical=['S6td'], decrement_radical=['S6td'], increment_lone_pair=['S4tdc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6tt'].set_actions(increment_bond=[], decrement_bond=['S6td', 'S6tdc'], form_bond=[], break_bond=[], increment_radical=[], decrement_radical=[], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['S6tdc'].set_actions(increment_bond=['S6td', 'S6tdc', 'S6tt'], decrement_bond=['S6dc', 'S6tdc'], form_bond=['S6tdc'], break_bond=['S6tdc'], increment_radical=['S6tdc'], decrement_radical=['S6tdc'], increment_lone_pair=['S4t', 'S4tdc'], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['Cl'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Cl'], break_bond=['Cl'], increment_radical=['Cl'], decrement_radical=['Cl'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Cl1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Cl1s'], break_bond=['Cl1s'], increment_radical=['Cl1s'], decrement_radical=['Cl1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['Br'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Br'], break_bond=['Br'], increment_radical=['Br'], decrement_radical=['Br'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['Br1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['Br1s'], break_bond=['Br1s'], increment_radical=['Br1s'], decrement_radical=['Br1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['I'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['I'], break_bond=['I'], increment_radical=['I'], decrement_radical=['I'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['I1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['I1s'], break_bond=['I1s'], increment_radical=['I1s'], decrement_radical=['I1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) + +ATOMTYPES['F'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F'], break_bond=['F'], increment_radical=['F'], decrement_radical=['F'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) +ATOMTYPES['F1s'].set_actions(increment_bond=[], decrement_bond=[], form_bond=['F1s'], break_bond=['F1s'], increment_radical=['F1s'], decrement_radical=['F1s'], increment_lone_pair=[], decrement_lone_pair=[], increment_charge=[], decrement_charge=[]) # these are ordered in priority of picking if a more general atomtype is encountered -allElements = ['H', 'C', 'O', 'N', 'S', 'P', 'Si', 'F', 'Cl', 'Br', 'I', 'Ne', 'Ar', 'He', 'X', 'Li'] +allElements = ['H', 'C', 'O', 'N', 'S', 'P', 'Si', 'F', 'Cl', 'Br', 'I', 'Li', 'Ne', 'Ar', 'He', 'X', 'e', ] # list of elements that do not have more specific atomTypes -nonSpecifics = ['H', 'He', 'Ne', 'Ar'] +nonSpecifics = ['He', 'Ne', 'Ar', 'e'] for atomtype in ATOMTYPES.values(): for items in [atomtype.generic, atomtype.specific, atomtype.increment_bond, atomtype.decrement_bond, atomtype.form_bond, atomtype.break_bond, atomtype.increment_radical, atomtype.decrement_radical, atomtype.increment_lone_pair, - atomtype.decrement_lone_pair]: + atomtype.decrement_lone_pair, atomtype.increment_charge, atomtype.decrement_charge]: for index in range(len(items)): items[index] = ATOMTYPES[items[index]] - def get_features(atom, bonds): """ Returns a list of features needed to determine atomtype for :class:'Atom' diff --git a/molecule/molecule/converter.py b/molecule/molecule/converter.py index 0368bdf..7a7a7d7 100644 --- a/molecule/molecule/converter.py +++ b/molecule/molecule/converter.py @@ -116,6 +116,9 @@ def to_rdkit_mol(mol, remove_h=True, return_mapping=False, sanitize=True, save_o for label, ind_list in label_dict.items(): for ind in ind_list: Chem.SetSupplementalSmilesLabel(rdkitmol.GetAtomWithIdx(ind), label) + for atom in rdkitmol.GetAtoms(): + if atom.GetAtomicNum() > 1: + atom.SetNoImplicit(True) if sanitize: Chem.SanitizeMol(rdkitmol) if remove_h: diff --git a/molecule/molecule/draw.py b/molecule/molecule/draw.py index 62c4fa2..d9ff3af 100644 --- a/molecule/molecule/draw.py +++ b/molecule/molecule/draw.py @@ -49,6 +49,7 @@ import math import os.path import re +import itertools try: import cairocffi as cairo @@ -60,7 +61,8 @@ import numpy as np from rdkit.Chem import AllChem -from molecule.molecule.molecule import Atom, Molecule +from molecule.molecule.molecule import Atom, Molecule, Bond +from molecule.molecule.pathfinder import find_shortest_path from molecule.qm.molecule import Geometry @@ -96,14 +98,20 @@ def create_new_surface(file_format, target=None, width=1024, height=768): ################################################################################ +class AdsorbateDrawingError(Exception): + """ + When something goes wrong trying to draw an adsorbate. + """ + pass + class MoleculeDrawer(object): """ This class provides functionality for drawing the skeletal formula of molecules using the Cairo 2D graphics engine. The most common use case is simply:: - + MoleculeDrawer().draw(molecule, file_format='png', path='molecule.png') - + where ``molecule`` is the :class:`Molecule` object to draw. You can also pass a dict of options to the constructor to affect how the molecules are drawn. @@ -176,7 +184,8 @@ def draw(self, molecule, file_format, target=None): surface_sites = [] for atom in self.molecule.atoms: if isinstance(atom, Atom) and atom.is_hydrogen() and atom.label == '': - atoms_to_remove.append(atom) + if not any(bond.is_hydrogen_bond() for bond in atom.bonds.values()): + atoms_to_remove.append(atom) elif atom.is_surface_site(): surface_sites.append(atom) if len(atoms_to_remove) < len(self.molecule.atoms) - len(surface_sites): @@ -199,7 +208,7 @@ def draw(self, molecule, file_format, target=None): self.molecule.remove_atom(atom) self.symbols = ['CO'] self.molecule.atoms[0].charge = 0 # don't label the C as - if you're not drawing the O with a + - self.coordinates = np.array([[0, 0]], np.float64) + self.coordinates = np.array([[0, 0]], float) else: # Generate the coordinates to use to draw the molecule try: @@ -207,7 +216,16 @@ def draw(self, molecule, file_format, target=None): # replace the bonds after generating coordinates. This avoids # bugs with RDKit old_bond_dictionary = self._make_single_bonds() - self._generate_coordinates() + if molecule.contains_surface_site(): + try: + self._connect_surface_sites() + self._generate_coordinates() + self._disconnect_surface_sites() + except AdsorbateDrawingError as e: + self._disconnect_surface_sites() + self._generate_coordinates(fix_surface_sites=False) + else: + self._generate_coordinates() self._replace_bonds(old_bond_dictionary) # Generate labels to use @@ -234,34 +252,34 @@ def draw(self, molecule, file_format, target=None): # Render as H2 instead of H-H self.molecule.remove_atom(self.molecule.atoms[-1]) self.symbols = ['H2'] - self.coordinates = np.array([[0, 0]], np.float64) + self.coordinates = np.array([[0, 0]], float) elif molecule.is_isomorphic(Molecule(smiles='[O][O]')): # Render as O2 instead of O-O self.molecule.remove_atom(self.molecule.atoms[-1]) self.molecule.atoms[0].radical_electrons = 0 self.symbols = ['O2'] - self.coordinates = np.array([[0, 0]], np.float64) + self.coordinates = np.array([[0, 0]], float) elif self.symbols == ['OH', 'O'] or self.symbols == ['O', 'OH']: # Render as HO2 instead of HO-O or O-OH self.molecule.remove_atom(self.molecule.atoms[-1]) self.symbols = ['O2H'] - self.coordinates = np.array([[0, 0]], np.float64) + self.coordinates = np.array([[0, 0]], float) elif self.symbols == ['OH', 'OH']: # Render as H2O2 instead of HO-OH or O-OH self.molecule.remove_atom(self.molecule.atoms[-1]) self.symbols = ['O2H2'] - self.coordinates = np.array([[0, 0]], np.float64) + self.coordinates = np.array([[0, 0]], float) elif self.symbols == ['O', 'C', 'O']: # Render as CO2 instead of O=C=O self.molecule.remove_atom(self.molecule.atoms[0]) self.molecule.remove_atom(self.molecule.atoms[-1]) self.symbols = ['CO2'] - self.coordinates = np.array([[0, 0]], np.float64) + self.coordinates = np.array([[0, 0]], float) elif self.symbols == ['H', 'H', 'X']: # Render as H2::X instead of crashing on H-H::X (vdW bond) self.molecule.remove_atom(self.molecule.atoms[0]) self.symbols = ['H2', 'X'] - self.coordinates = np.array([[0, -0.5], [0, 0.5]], np.float64) * self.options['bondLength'] + self.coordinates = np.array([[0, -0.5], [0, 0.5]], float) * self.options['bondLength'] # Create a dummy surface to draw to, since we don't know the bounding rect # We will copy this to another surface with the correct bounding rect @@ -323,11 +341,13 @@ def _find_ring_groups(self): if not found: self.ringSystems.append([cycle]) - def _generate_coordinates(self): + def _generate_coordinates(self, fix_surface_sites=True): """ - Generate the 2D coordinates to be used when drawing the current + Generate the 2D coordinates to be used when drawing the current molecule. The function uses rdKits 2D coordinate generation. Updates the self.coordinates Array in place. + If `fix_surface_sites` is True, then the surface sites are placed + at the bottom of the molecule. """ atoms = self.molecule.atoms natoms = len(atoms) @@ -383,26 +403,13 @@ def _generate_coordinates(self): else: angle = math.atan2(vector0[0], vector0[1]) - math.pi / 2 rot = np.array([[math.cos(angle), math.sin(angle)], - [-math.sin(angle), math.cos(angle)]], np.float64) + [-math.sin(angle), math.cos(angle)]], float) # need to keep self.coordinates and coordinates referring to the same object self.coordinates = coordinates = np.dot(coordinates, rot) - - # If two atoms lie on top of each other, push them apart a bit - # This is ugly, but at least the mess you end up with isn't as misleading - # as leaving everything piled on top of each other at the origin - import itertools - for atom1, atom2 in itertools.combinations(backbone, 2): - i1, i2 = atoms.index(atom1), atoms.index(atom2) - if np.linalg.norm(coordinates[i1, :] - coordinates[i2, :]) < 0.5: - coordinates[i1, 0] -= 0.3 - coordinates[i2, 0] += 0.3 - coordinates[i1, 1] -= 0.2 - coordinates[i2, 1] += 0.2 # If two atoms lie on top of each other, push them apart a bit # This is ugly, but at least the mess you end up with isn't as misleading # as leaving everything piled on top of each other at the origin - import itertools for atom1, atom2 in itertools.combinations(backbone, 2): i1, i2 = atoms.index(atom1), atoms.index(atom2) if np.linalg.norm(coordinates[i1, :] - coordinates[i2, :]) < 0.5: @@ -457,26 +464,59 @@ def _generate_coordinates(self): coordinates[:, 0] = temp[:, 1] coordinates[:, 1] = temp[:, 0] - # For surface species, rotate them so the site is at the bottom. - if self.molecule.contains_surface_site(): + # For surface species + if fix_surface_sites and self.molecule.contains_surface_site(): if len(self.molecule.atoms) == 1: return coordinates - for site in self.molecule.atoms: - if site.is_surface_site(): - break - else: - raise Exception("Can't find surface site") - if site.bonds: - adsorbate = next(iter(site.bonds)) - vector0 = coordinates[atoms.index(site), :] - coordinates[atoms.index(adsorbate), :] - angle = math.atan2(vector0[0], vector0[1]) - math.pi - rot = np.array([[math.cos(angle), math.sin(angle)], [-math.sin(angle), math.cos(angle)]], np.float64) + sites = [atom for atom in self.molecule.atoms if atom.is_surface_site()] + if len(sites) == 1: + # rotate them so the site is at the bottom. + site = sites[0] + if site.bonds: + adatom = next(iter(site.bonds)) + vector0 = coordinates[atoms.index(site), :] - coordinates[atoms.index(adatom), :] + angle = math.atan2(vector0[0], vector0[1]) - math.pi + rot = np.array([[math.cos(angle), math.sin(angle)], [-math.sin(angle), math.cos(angle)]], float) + self.coordinates = coordinates = np.dot(coordinates, rot) + else: + # van der Waals + index = atoms.index(site) + coordinates[index, 1] = min(coordinates[:, 1]) - 0.8 # just move the site down a bit + coordinates[index, 0] = coordinates[:, 0].mean() # and center it + elif len(sites) <= 4: + # Rotate so the line of best fit through the adatoms is horizontal. + # find atoms bonded to sites + adatoms = [next(iter(site.bonds)) for site in sites] + adatom_indices = [atoms.index(a) for a in adatoms] + # find the best fit line through the bonded atoms + x = coordinates[adatom_indices, 0] + y = coordinates[adatom_indices, 1] + A = np.vstack([x, np.ones(len(x))]).T + m, c = np.linalg.lstsq(A, y, rcond=None)[0] + # rotate so the line is horizontal + angle = -math.atan(m) + rot = np.array([[math.cos(angle), math.sin(angle)], [-math.sin(angle), math.cos(angle)]], float) self.coordinates = coordinates = np.dot(coordinates, rot) + # if the line is above the middle, flip it + not_site_indices = [atoms.index(a) for a in atoms if not a.is_surface_site()] + if coordinates[adatom_indices, 1].mean() > coordinates[not_site_indices, 1].mean(): + coordinates[:, 1] *= -1 + x = coordinates[adatom_indices, 0] + y = coordinates[adatom_indices, 1] + site_y_pos = min(min(y) - 0.8, min(coordinates[not_site_indices, 1]) - 0.5) + if max(y) - site_y_pos > 1.5: + raise AdsorbateDrawingError("Adsorbate bond too long") + for x1, x2 in itertools.combinations(x, 2): + if abs(x1 - x2) < 0.2: + raise AdsorbateDrawingError("Sites overlapping") + for site, x_pos in zip(sites, x): + index = atoms.index(site) + coordinates[index, 1] = site_y_pos + coordinates[index, 0] = x_pos + else: - # van der waals - index = atoms.index(site) - coordinates[index, 1] = min(coordinates[:, 1]) - 0.8 # just move the site down a bit - coordinates[index, 0] = coordinates[:, 0].mean() # and center it + # more than 4 surface sites? leave them alone + pass def _find_cyclic_backbone(self): """ @@ -593,7 +633,7 @@ def _generate_ring_system_coordinates(self, atoms): found = False common_atoms = [] count = 0 - center0 = np.zeros(2, np.float64) + center0 = np.zeros(2, float) for cycle1 in processed: found = False for atom in cycle1: @@ -601,7 +641,7 @@ def _generate_ring_system_coordinates(self, atoms): common_atoms.append(atom) found = True if found: - center1 = np.zeros(2, np.float64) + center1 = np.zeros(2, float) for atom in cycle1: center1 += coordinates[cycle1.index(atom), :] center1 /= len(cycle1) @@ -624,7 +664,7 @@ def _generate_ring_system_coordinates(self, atoms): if len(common_atoms) == 1 or len(common_atoms) == 2: # Center of new cycle is reflection of center of adjacent cycle # across common atom or bond - center = np.zeros(2, np.float64) + center = np.zeros(2, float) for atom in common_atoms: center += coordinates[self.molecule.atoms.index(atom), :] center /= len(common_atoms) @@ -638,8 +678,8 @@ def _generate_ring_system_coordinates(self, atoms): index0 = self.molecule.atoms.index(common_atoms[0]) index1 = self.molecule.atoms.index(common_atoms[1]) index2 = self.molecule.atoms.index(common_atoms[2]) - A = np.zeros((2, 2), np.float64) - b = np.zeros((2), np.float64) + A = np.zeros((2, 2), float) + b = np.zeros((2), float) A[0, :] = 2 * (coordinates[index1, :] - coordinates[index0, :]) A[1, :] = 2 * (coordinates[index2, :] - coordinates[index0, :]) b[0] = coordinates[index1, 0] ** 2 + coordinates[index1, 1] ** 2 - coordinates[index0, 0] ** 2 - coordinates[index0, 1] ** 2 @@ -676,7 +716,7 @@ def _generate_ring_system_coordinates(self, atoms): # This version assumes that no atoms belong at the origin, which is # usually fine because the first ring is centered at the origin if np.linalg.norm(coordinates[index, :]) < 1e-4: - vector = np.array([math.cos(angle), math.sin(angle)], np.float64) + vector = np.array([math.cos(angle), math.sin(angle)], float) coordinates[index, :] = center + radius * vector count += 1 @@ -686,7 +726,7 @@ def _generate_ring_system_coordinates(self, atoms): def _generate_straight_chain_coordinates(self, atoms): """ Update the coordinates for the linear straight chain of `atoms` in - the current molecule. + the current molecule. """ coordinates = self.coordinates @@ -696,14 +736,14 @@ def _generate_straight_chain_coordinates(self, atoms): # Second atom goes on x-axis (for now; this could be improved!) index1 = self.molecule.atoms.index(atoms[1]) - vector = np.array([1.0, 0.0], np.float64) + vector = np.array([1.0, 0.0], float) if atoms[0].bonds[atoms[1]].is_triple(): rotate_positive = False else: rotate_positive = True rot = np.array([[math.cos(-math.pi / 6), math.sin(-math.pi / 6)], - [-math.sin(-math.pi / 6), math.cos(-math.pi / 6)]], np.float64) - vector = np.array([1.0, 0.0], np.float64) + [-math.sin(-math.pi / 6), math.cos(-math.pi / 6)]], float) + vector = np.array([1.0, 0.0], float) vector = np.dot(rot, vector) coordinates[index1, :] = coordinates[index0, :] + vector @@ -740,7 +780,7 @@ def _generate_straight_chain_coordinates(self, atoms): # Determine coordinates for atom if angle != 0: if not rotate_positive: angle = -angle - rot = np.array([[math.cos(angle), math.sin(angle)], [-math.sin(angle), math.cos(angle)]], np.float64) + rot = np.array([[math.cos(angle), math.sin(angle)], [-math.sin(angle), math.cos(angle)]], float) vector = np.dot(rot, vector) rotate_positive = not rotate_positive coordinates[index2, :] = coordinates[index1, :] + vector @@ -780,7 +820,7 @@ def _generate_neighbor_coordinates(self, backbone): # Determine rotation angle and matrix rot = np.array([[math.cos(best_angle), -math.sin(best_angle)], - [math.sin(best_angle), math.cos(best_angle)]], np.float64) + [math.sin(best_angle), math.cos(best_angle)]], float) # Determine the vector of any currently-existing bond from this atom vector = None for atom1 in atom0.bonds: @@ -824,7 +864,7 @@ def _generate_neighbor_coordinates(self, backbone): if atom1 not in backbone and np.linalg.norm(coordinates[atoms.index(atom1), :]) < 1e-4: angle = start_angle + index * d_angle index += 1 - vector = np.array([math.cos(angle), math.sin(angle)], np.float64) + vector = np.array([math.cos(angle), math.sin(angle)], float) vector /= np.linalg.norm(vector) coordinates[atoms.index(atom1), :] = coordinates[index0, :] + vector self._generate_functional_group_coordinates(atom0, atom1) @@ -854,7 +894,7 @@ def _generate_functional_group_coordinates(self, atom0, atom1): # Check to see if atom1 is in any cycles in the molecule ring_system = None for ring_sys in self.ringSystems: - if any([atom1 in ring for ring in ring_sys]): + if any(atom1 in ring for ring in ring_sys): ring_system = ring_sys if ring_system is not None: @@ -873,13 +913,13 @@ def _generate_functional_group_coordinates(self, atom0, atom1): # Rotate the ring system coordinates so that the line connecting atom1 # and the center of mass of the ring is parallel to that between # atom0 and atom1 - center = np.zeros(2, np.float64) + center = np.zeros(2, float) for atom in cycle_atoms: center += coordinates_cycle[atoms.index(atom), :] center /= len(cycle_atoms) vector0 = center - coordinates_cycle[atoms.index(atom1), :] angle = math.atan2(vector[1] - vector0[1], vector[0] - vector0[0]) - rot = np.array([[math.cos(angle), -math.sin(angle)], [math.sin(angle), math.cos(angle)]], np.float64) + rot = np.array([[math.cos(angle), -math.sin(angle)], [math.sin(angle), math.cos(angle)]], float) coordinates_cycle = np.dot(coordinates_cycle, rot) # Translate the ring system coordinates to the position of atom1 @@ -905,8 +945,8 @@ def _generate_functional_group_coordinates(self, atom0, atom1): angle = 2 * math.pi / 3 # Make sure we're rotating such that we move away from the origin, # to discourage overlap of functional groups - rot1 = np.array([[math.cos(angle), -math.sin(angle)], [math.sin(angle), math.cos(angle)]], np.float64) - rot2 = np.array([[math.cos(angle), math.sin(angle)], [-math.sin(angle), math.cos(angle)]], np.float64) + rot1 = np.array([[math.cos(angle), -math.sin(angle)], [math.sin(angle), math.cos(angle)]], float) + rot2 = np.array([[math.cos(angle), math.sin(angle)], [-math.sin(angle), math.cos(angle)]], float) vector1 = coordinates[index1, :] + np.dot(rot1, vector) vector2 = coordinates[index1, :] + np.dot(rot2, vector) if bond_angle < -0.5 * math.pi or bond_angle > 0.5 * math.pi: @@ -915,7 +955,7 @@ def _generate_functional_group_coordinates(self, atom0, atom1): angle = -abs(angle) else: angle = 2 * math.pi / num_bonds - rot = np.array([[math.cos(angle), -math.sin(angle)], [math.sin(angle), math.cos(angle)]], np.float64) + rot = np.array([[math.cos(angle), -math.sin(angle)], [math.sin(angle), math.cos(angle)]], float) # Iterate through each neighboring atom to this backbone atom # If the neighbor is not in the backbone, then we need to determine @@ -1024,7 +1064,7 @@ def render(self, cr, offset=None): cycle_bonds.append(cycle[0].bonds[cycle[-1]]) if all([bond.is_benzene() for bond in cycle_bonds]): # We've found an aromatic ring, so draw a circle in the center to represent the benzene bonds - center = np.zeros(2, np.float64) + center = np.zeros(2, float) for atom in cycle: index = atoms.index(atom) center += coordinates[index, :] @@ -1046,7 +1086,7 @@ def render(self, cr, offset=None): symbol = symbols[i] index = atoms.index(atom) x0, y0 = coordinates[index, :] - vector = np.zeros(2, np.float64) + vector = np.zeros(2, float) for atom2 in atom.bonds: vector += coordinates[atoms.index(atom2), :] - coordinates[index, :] heavy_first = vector[0] <= 0 @@ -1334,6 +1374,8 @@ def _render_atom(self, symbol, atom, x0, y0, cr, heavy_first=True, draw_lone_pai cr.set_source_rgba(0.5, 0.0, 0.5, 1.0) elif heavy_atom == 'X': cr.set_source_rgba(0.5, 0.25, 0.5, 1.0) + elif heavy_atom == 'e': + cr.set_source_rgba(1.0, 0.0, 1.0, 1.0) else: cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) @@ -1386,7 +1428,7 @@ def _render_atom(self, symbol, atom, x0, y0, cr, heavy_first=True, draw_lone_pai # Internal atom # First try to see if there is a "preferred" side on which to place the # radical/charge data, i.e. if the bonds are unbalanced - vector = np.zeros(2, np.float64) + vector = np.zeros(2, float) for atom1 in atom.bonds: vector += self.coordinates[atoms.index(atom), :] - self.coordinates[atoms.index(atom1), :] if np.linalg.norm(vector) < 1e-4: @@ -1527,7 +1569,7 @@ def _render_atom(self, symbol, atom, x0, y0, cr, heavy_first=True, draw_lone_pai cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.show_text(text) - # Draw lone electron pairs + # Draw lone electron pairs # Draw them for nitrogen containing molecules only if draw_lone_pairs: for i in range(atom.lone_pairs): @@ -1624,6 +1666,40 @@ def _replace_bonds(self, bond_order_dictionary): for bond, order in bond_order_dictionary.items(): bond.set_order_num(order) + def _connect_surface_sites(self): + """ + Creates single bonds between atoms that are surface sites. + This is to help make multidentate adsorbates look better. + """ + sites = [a for a in self.molecule.atoms if a.is_surface_site()] + if len(sites) > 4: + return + for site1 in sites: + other_sites = [a for a in sites if a != site1] + if not other_sites: break + # connect to the nearest site + site2 = min(other_sites, key=lambda a: len(find_shortest_path(site1, a))) + if len(find_shortest_path(site1, site2)) > 2 and len(sites) > 3: + # if there are more than 3 sites, don't connect sites that aren't neighbors + continue + + bond = site1.bonds.get(site2) + if bond is None: + bond = Bond(site1, site2, 1) + site1.bonds[site2] = bond + site2.bonds[site1] = bond + + def _disconnect_surface_sites(self): + """ + Removes all bonds between atoms that are surface sites. + """ + for site1 in self.molecule.atoms: + if site1.is_surface_site(): + for site2 in list(site1.bonds.keys()): # make a list copy so we can delete from the dict + if site2.is_surface_site(): + del site1.bonds[site2] + del site2.bonds[site1] + ################################################################################ @@ -1632,9 +1708,9 @@ class ReactionDrawer(object): This class provides functionality for drawing chemical reactions using the skeletal formula of each reactant and product molecule via the Cairo 2D graphics engine. The most common use case is simply:: - + ReactionDrawer().draw(reaction, file_format='png', path='reaction.png') - + where ``reaction`` is the :class:`Reaction` object to draw. You can also pass a dict of options to the constructor to affect how the molecules are drawn. @@ -1644,6 +1720,7 @@ def __init__(self, options=None): self.options = MoleculeDrawer().options.copy() self.options.update({ 'arrowLength': 36, + 'drawReversibleArrow': True }) if options: self.options.update(options) @@ -1653,7 +1730,7 @@ def draw(self, reaction, file_format, path=None): Draw the given `reaction` using the given image `file_format` - pdf, svg, ps, or png. If `path` is given, the drawing is saved to that location on disk. - + This function returns the Cairo surface and context used to create the drawing, as well as a bounding box for the molecule being drawn as the tuple (`left`, `top`, `width`, `height`). @@ -1744,11 +1821,30 @@ def draw(self, reaction, file_format, path=None): rxn_cr.save() rxn_cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) rxn_cr.set_line_width(1.0) - rxn_cr.move_to(rxn_x + 8, rxn_top + 0.5 * rxn_height) - rxn_cr.line_to(rxn_x + arrow_width - 8, rxn_top + 0.5 * rxn_height) - rxn_cr.move_to(rxn_x + arrow_width - 14, rxn_top + 0.5 * rxn_height - 3.0) - rxn_cr.line_to(rxn_x + arrow_width - 8, rxn_top + 0.5 * rxn_height) - rxn_cr.line_to(rxn_x + arrow_width - 14, rxn_top + 0.5 * rxn_height + 3.0) + if self.options['drawReversibleArrow'] and reaction.reversible: # draw double harpoons + TOP_HARPOON_Y = rxn_top + (0.5 * rxn_height - 1.75) + BOTTOM_HARPOON_Y = rxn_top + (0.5 * rxn_height + 1.75) + + # Draw top harpoon + rxn_cr.move_to(rxn_x + 8, TOP_HARPOON_Y) + rxn_cr.line_to(rxn_x + arrow_width - 8, TOP_HARPOON_Y) + rxn_cr.move_to(rxn_x + arrow_width - 14, TOP_HARPOON_Y - 3.0) + rxn_cr.line_to(rxn_x + arrow_width - 8, TOP_HARPOON_Y) + + # Draw bottom harpoon + rxn_cr.move_to(rxn_x + arrow_width - 8, BOTTOM_HARPOON_Y) + rxn_cr.line_to(rxn_x + 8, BOTTOM_HARPOON_Y) + rxn_cr.move_to(rxn_x + 14, BOTTOM_HARPOON_Y + 3.0) + rxn_cr.line_to(rxn_x + 8, BOTTOM_HARPOON_Y) + + + else: # draw forward arrow + rxn_cr.move_to(rxn_x + 8, rxn_top + 0.5 * rxn_height) + rxn_cr.line_to(rxn_x + arrow_width - 8, rxn_top + 0.5 * rxn_height) + rxn_cr.move_to(rxn_x + arrow_width - 14, rxn_top + 0.5 * rxn_height - 3.0) + rxn_cr.line_to(rxn_x + arrow_width - 8, rxn_top + 0.5 * rxn_height) + rxn_cr.line_to(rxn_x + arrow_width - 14, rxn_top + 0.5 * rxn_height + 3.0) + rxn_cr.stroke() rxn_cr.restore() rxn_x += arrow_width diff --git a/molecule/molecule/element.py b/molecule/molecule/element.py index ba3688f..91da069 100644 --- a/molecule/molecule/element.py +++ b/molecule/molecule/element.py @@ -78,7 +78,7 @@ def __init__(self, number, symbol, name, mass, isotope=-1, chemkin_name=None): self.mass = mass self.isotope = isotope self.chemkin_name = chemkin_name or self.name - if symbol == 'X': + if symbol in {'X','L','R','e'}: self.cov_radius = 0 else: try: @@ -122,14 +122,15 @@ class PeriodicSystem(object): https://sciencenotes.org/list-of-electronegativity-values-of-the-elements/ isotopes of the same element may have slight different electronegativities, which is not reflected below """ - valences = {'H': 1, 'He': 0, 'C': 4, 'N': 3, 'O': 2, 'F': 1, 'Ne': 0, + valences = {'H+':0, 'e': 0, 'H': 1, 'He': 0, 'C': 4, 'N': 3, 'O': 2, 'F': 1, 'Ne': 0, 'Si': 4, 'P': 3, 'S': 2, 'Cl': 1, 'Br': 1, 'Ar': 0, 'I': 1, 'X': 4, 'Li': 1} - valence_electrons = {'H': 1, 'He': 2, 'C': 4, 'N': 5, 'O': 6, 'F': 7, 'Ne': 8, + valence_electrons = {'H+':0, 'e': 1, 'H': 1, 'He': 2, 'C': 4, 'N': 5, 'O': 6, 'F': 7, 'Ne': 8, 'Si': 4, 'P': 5, 'S': 6, 'Cl': 7, 'Br': 7, 'Ar': 8, 'I': 7, 'X': 4, 'Li': 1} - lone_pairs = {'H': 0, 'He': 1, 'C': 0, 'N': 1, 'O': 2, 'F': 3, 'Ne': 4, + lone_pairs = {'H+':0, 'e': 0, 'H': 0, 'He': 1, 'C': 0, 'N': 1, 'O': 2, 'F': 3, 'Ne': 4, 'Si': 0, 'P': 1, 'S': 2, 'Cl': 3, 'Br': 3, 'Ar': 4, 'I': 3, 'X': 0, 'Li': 0} electronegativity = {'H': 2.20, 'D': 2.20, 'T': 2.20, 'C': 2.55, 'C13': 2.55, 'N': 3.04, 'O': 3.44, 'O18': 3.44, - 'F': 3.98, 'Si': 1.90, 'P': 2.19, 'S': 2.58, 'Cl': 3.16, 'Br': 2.96, 'I': 2.66, 'X': 0.0, 'Li': 0.98} + 'F': 3.98, 'Si': 1.90, 'P': 2.19, 'S': 2.58, 'Cl': 3.16, 'Br': 2.96, 'I': 2.66, 'X': 0.0, + 'Li' : 0.98} ################################################################################ @@ -173,6 +174,8 @@ def get_element(value, isotope=-1): # Recommended IUPAC nomenclature is used throughout (including 'aluminium' and # 'caesium') +# electron +e = Element(-1, 'e', 'electron' , 5.486e-7) # Surface site X = Element(0, 'X', 'surface_site' , 0.0) @@ -310,6 +313,7 @@ def get_element(value, isotope=-1): # A list of the elements, sorted by increasing atomic number element_list = [ + e, X, H, D, T, He, Li, Be, B, C, C13, N, O, O18, F, Ne, @@ -332,6 +336,8 @@ def get_element(value, isotope=-1): # P=P value is from: https://www2.chemistry.msu.edu/faculty/reusch/OrgPage/bndenrgy.htm # C#S is the value for [C+]#[S-] from 10.1002/chem.201002840 referenced relative to 0 K # X-O and X-X (X=F,Cl,Br) taken from https://labs.chem.ucsb.edu/zakarian/armen/11---bonddissociationenergy.pdf +# Li-C and Li-S are taken referenced from 0K from from http://staff.ustc.edu.cn/~luo971/2010-91-CRC-BDEs-Tables.pdf +# Li-N is a G3 calculation taken from https://doi.org/10.1021/jp050857o # The reference state is gaseous state at 298 K, but some of the values in the bde_dict might be coming from 0 K. # The bond dissociation energy at 298 K is greater than the bond dissociation energy at 0 K by 0.6 to 0.9 kcal/mol # (between RT and 3/2 RT), and this difference is usually much smaller than the uncertainty in the bond dissociation @@ -382,7 +388,6 @@ def get_element(value, isotope=-1): ('Li', 'C', 1): (214.6, 'kJ/mol'), ('Li', 'S', 1): (312.5, 'kJ/mol'), ('Li', 'N', 1): (302.4, 'kJ/mol')} - bdes = {} for key, value in bde_dict.items(): q = Quantity(value).value_si diff --git a/molecule/molecule/filtration.py b/molecule/molecule/filtration.py index 8c1e656..b723a14 100644 --- a/molecule/molecule/filtration.py +++ b/molecule/molecule/filtration.py @@ -67,15 +67,12 @@ def filter_structures(mol_list, mark_unreactive=True, allow_expanded_octet=True, #Remove structures that try to put negative charges on metal ions filtered_list = ionic_bond_filteration(mol_list) - # Get an octet deviation list octet_deviation_list = get_octet_deviation_list(filtered_list, allow_expanded_octet=allow_expanded_octet) - # Filter mol_list using the octet rule and the respective octet deviation list filtered_list, charge_span_list = octet_filtration(filtered_list, octet_deviation_list) - # Filter by charge filtered_list = charge_filtration(filtered_list, charge_span_list) @@ -96,6 +93,7 @@ def filter_structures(mol_list, mark_unreactive=True, allow_expanded_octet=True, return filtered_list + def ionic_bond_filteration(mol_list): """ Returns a filtered list removing structures that put a negative charge on lithium diff --git a/molecule/molecule/fragment.py b/molecule/molecule/fragment.py index 6fe3359..4672bd9 100644 --- a/molecule/molecule/fragment.py +++ b/molecule/molecule/fragment.py @@ -1,36 +1,69 @@ -import os -from urllib.parse import quote +#!/usr/bin/env python3 + +############################################################################### +# # +# RMG - Reaction Mechanism Generator # +# # +# Copyright (c) 2002-2023 Prof. William H. Green (whgreen@mit.edu), # +# Prof. Richard H. West (r.west@neu.edu) and the RMG Team (rmg_dev@mit.edu) # +# # +# Permission is hereby granted, free of charge, to any person obtaining a # +# copy of this software and associated documentation files (the 'Software'), # +# to deal in the Software without restriction, including without limitation # +# the rights to use, copy, modify, merge, publish, distribute, sublicense, # +# and/or sell copies of the Software, and to permit persons to whom the # +# Software is furnished to do so, subject to the following conditions: # +# # +# The above copyright notice and this permission notice shall be included in # +# all copies or substantial portions of the Software. # +# # +# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # +# DEALINGS IN THE SOFTWARE. # +# # +############################################################################### + import itertools import logging import molecule.molecule.group as gr import molecule.molecule.element as elements +from molecule.molecule.element import Element import molecule.molecule.converter as converter -import molecule.molecule.resonance as resonance from molecule.molecule.element import get_element from molecule.molecule.graph import Graph, Vertex from molecule.molecule.molecule import Atom, Bond, Molecule -from molecule.molecule.atomtype import get_atomtype, AtomTypeError -from molecule.molecule.kekulize import kekulize +from molecule.molecule.atomtype import get_atomtype, AtomTypeError, ATOMTYPES, AtomType from rdkit import Chem -from rdkit.Chem import AllChem -import numpy as np - -class CuttingLabel(Vertex): - - def __init__(self, name='', label='', id=-1): - Vertex.__init__(self) - self.name = name # equivalent to Atom element symbol - self.label = label # equivalent to Atom label attribute - self.charge = 0 - self.radical_electrons = 0 - self.lone_pairs = 0 - self.isotope = -1 - self.id = id - self.mass = 0 - self.site = '' - self.morphology = '' +# this variable is used to name atom IDs so that there are as few conflicts by +# using the entire space of integer objects +ATOM_ID_COUNTER = -(2**15) + + +class CuttingLabel(Atom): + def __init__(self, name="", label="", id=-1): + super().__init__( + element=Element(0, name, "cutting label", 0.0, -1), + radical_electrons=0, + charge=0, + label=label, + lone_pairs=0, + site="", + coords=None, + id=id, + props=None, + ) + # define a new custom atom type + self.atomtype = AtomType(label=label, generic=[], specific=[]) + # accessed by other methods in Fragment + self.name = name + self.isotope = -1 + def __str__(self): """ Return a human-readable string representation of the object. @@ -44,10 +77,8 @@ def __repr__(self): return "".format(str(self)) @property - def symbol(self): return self.name - - @property - def bonds(self): return self.edges + def symbol(self): + return self.name def is_specific_case_of(self, other): """ @@ -60,7 +91,7 @@ def equivalent(self, other, strict=True): """ Return ``True`` if `other` is indistinguishable from this CuttingLabel, or ``False`` otherwise. If `other` is an :class:`CuttingLabel` object, then all - attributes must match exactly. + attributes must match exactly. """ if isinstance(other, CuttingLabel): return self.name == other.name @@ -83,123 +114,59 @@ def copy(self): c.lone_pairs = self.lone_pairs c.isotope = self.isotope c.id = self.id + c.element = self.element + c.site = "" + c.morphology = "" return c - def is_carbon(self): - return False - - def is_nitrogen(self): - return False - - def is_oxygen(self): - return False - - def is_fluorine(self): - return False - - def is_surface_site(self): - return False - - def is_silicon(self): - return False - - def is_sulfur(self): - return False - - def is_chlorine(self): - return False - - def is_iodine(self): - return False - - def is_nos(self): - """ - Return ``True`` if the atom represent either nitrogen, sulfur, or oxygen - ``False`` if it does not. - """ - return False - - def is_non_hydrogen(self): - """ - Return ``True`` if the atom does not represent a hydrogen atom or - ``False`` if it does. - """ - return True - - -class Fragment(Graph): - def __init__(self, - label='', - species_repr=None, - vertices=None, - symmetry=-1, - multiplicity=-187, - reactive=True, - props=None, - inchi='', - smiles=''): +class Fragment(Molecule): + def __init__( + self, + label="", + species_repr=None, + vertices=None, + symmetry=-1, + multiplicity=-187, + reactive=True, + props=None, + inchi="", + smiles="", + ): + if inchi and smiles: + logging.warning( + "Both InChI and SMILES provided for Fragment instantiation, " + "using InChI and ignoring SMILES." + ) + smiles = "" + + super().__init__( + atoms=vertices, + symmetry=symmetry, + multiplicity=multiplicity, + reactive=reactive, + props=props, + inchi=inchi, + smiles=smiles, + metal="", + facet="", + ) self.index = -1 self.label = label self.species_repr = species_repr - Graph.__init__(self, vertices) - self.symmetry_number = symmetry - self._fingerprint = None - self._inchi = None - self._smiles = None - self.props = props or {} - self.multiplicity = multiplicity - self.reactive = reactive - self.metal = '' - self.facet = '' - - if inchi and smiles: - logging.warning('Both InChI and SMILES provided for Fragment instantiation, ' - 'using InChI and ignoring SMILES.') - if inchi: - self.from_inchi(inchi) - self._inchi = inchi - elif smiles: - self.from_smiles_like_string(smiles) - self._smiles = smiles - - def __deepcopy__(self, memo): - return self.copy(deep=True) def __str__(self): """ Return a string representation, in the form 'label(id)'. """ - if self.index == -1: return self.label - else: return '{0}({1:d})'.format(self.label, self.index) - - def __getAtoms(self): return self.vertices - def __setAtoms(self, atoms): self.vertices = atoms - atoms = property(__getAtoms, __setAtoms) - - def draw(self, path): - """ - Generate a pictorial representation of the chemical graph using the - :mod:`draw` module. Use `path` to specify the file to save - the generated image to; the image type is automatically determined by - extension. Valid extensions are ``.png``, ``.svg``, ``.pdf``, and - ``.ps``; of these, the first is a raster format and the remainder are - vector formats. - """ - from molecule.molecule.draw import MoleculeDrawer - format = os.path.splitext(path)[-1][1:].lower() - MoleculeDrawer().draw(self, format, target=path) + if self.index == -1: + return self.label + else: + return "{0}({1:d})".format(self.label, self.index) - def _repr_png_(self): - """ - Return a png picture of the molecule, useful for ipython-qtconsole. - """ - from molecule.molecule.draw import MoleculeDrawer - tempFileName = 'temp_molecule.png' - MoleculeDrawer().draw(self, 'png', tempFileName) - png = open(tempFileName,'rb').read() - os.unlink(tempFileName) - return png + # override methods + def is_lithium(self): + return False def copy(self, deep=False): """ @@ -222,167 +189,19 @@ def copy(self, deep=False): other.reactive = self.reactive return other - def clear_labeled_atoms(self): - """ - Remove the labels from all atoms in the fragment. - """ - for v in self.vertices: - v.label = '' - - def contains_labeled_atom(self, label): - """ - Return :data:`True` if the fragment contains an atom with the label - `label` and :data:`False` otherwise. - """ - for v in self.vertices: - if v.label == label: return True - return False - - def get_labeled_atoms(self, label): - """ - Return the atoms in the fragment that are labeled. - """ - alist = [v for v in self.vertices if v.label == label] - if alist == []: - raise ValueError( - 'No vertex in the fragment \n{1}\n has the label "{0}".'.format(label, self.to_adjacency_list())) - return alist - - def get_all_labeled_atoms(self): - """ - Return the labeled atoms as a ``dict`` with the keys being the labels - and the values the atoms themselves. If two or more atoms have the - same label, the value is converted to a list of these atoms. - """ - labeled = {} - for v in self.vertices: - if v.label != '': - if v.label in labeled: - if isinstance(labeled[v.label],list): - labeled[v.label].append(v) - else: - labeled[v.label] = [labeled[v.label]] - labeled[v.label].append(v) - else: - labeled[v.label] = v - return labeled - - @property - def fingerprint(self): - """ - Fingerprint used to accelerate graph isomorphism comparisons with - other molecules. The fingerprint is a short string containing a - summary of selected information about the molecule. Two fingerprint - strings matching is a necessary (but not sufficient) condition for - the associated molecules to be isomorphic. - - Use an expanded molecular formula to also enable sorting. - """ - if self._fingerprint is None: - # Include these elements in this order at minimum - element_dict = {'C': 0, 'H': 0, 'N': 0, 'O': 0, 'S': 0} - all_elements = sorted(self.get_element_count().items(), key=lambda x: x[0]) # Sort alphabetically - element_dict.update(all_elements) - self._fingerprint = ''.join([f'{symbol}{num:0>2}' for symbol, num in element_dict.items()]) - return self._fingerprint - - @fingerprint.setter - def fingerprint(self, fingerprint): - self._fingerprint = fingerprint - - @property - def inchi(self): - """InChI string for this molecule. Read-only.""" - if self._inchi is None: - self._inchi = self.to_inchi() - return self._inchi - - @property - def smiles(self): - """SMILES string for this molecule. Read-only.""" - if self._smiles is None: - self._smiles = self.to_smiles() - return self._smiles - - def add_atom(self, atom): - """ - Add an `atom` to the graph. The atom is initialized with no bonds. - """ - self._fingerprint = self._inchi = self._smiles = None - return self.add_vertex(atom) - - def remove_atom(self, atom): - """ - Remove `atom` and all bonds associated with it from the graph. Does - not remove atoms that no longer have any bonds as a result of this - removal. - """ - self._fingerprint = self._inchi = self._smiles = None - return self.remove_vertex(atom) - def contains_surface_site(self): """ Returns ``True`` iff the molecule contains an 'X' surface site. """ for atom in self.vertices: - if atom.symbol == 'X': + if atom.symbol == "X": return True return False def is_surface_site(self): "Returns ``True`` iff the molecule is nothing but a surface site 'X'." return len(self.vertices) == 1 and self.vertices[0].is_surface_site() - - def has_bond(self, atom1, atom2): - """ - Returns ``True`` if atoms `atom1` and `atom2` are connected - by an bond, or ``False`` if not. - """ - return self.has_edge(atom1, atom2) - - def get_bond(self, atom1, atom2): - """ - Returns the bond connecting atoms `atom1` and `atom2`. - """ - return self.get_edge(atom1, atom2) - - def add_bond(self, bond): - """ - Add a `bond` to the graph as an edge connecting the two atoms `atom1` - and `atom2`. - """ - self._fingerprint = self._inchi = self._smiles = None - return self.add_edge(bond) - - def remove_bond(self, bond): - """ - Remove the bond between atoms `atom1` and `atom2` from the graph. - Does not remove atoms that no longer have any bonds as a result of - this removal. - """ - self._fingerprint = self._inchi = self._smiles = None - return self.remove_edge(bond) - - def get_net_charge(self): - """ - Iterate through the atoms in the structure and calculate the net charge - on the overall fragment. - """ - charge = 0 - for v in self.vertices: - charge += v.charge - return charge - - def get_charge_span(self): - """ - Iterate through the atoms in the structure and calculate the charge span - on the overall molecule. - The charge span is a measure of the number of charge separations in a molecule. - """ - abs_net_charge = abs(self.get_net_charge()) - sum_of_abs_charges = sum([abs(atom.charge) for atom in self.vertices]) - return (sum_of_abs_charges - abs_net_charge) / 2 - + def merge(self, other): """ Merge two fragments so as to store them in a single :class:`Fragment` @@ -415,7 +234,7 @@ def get_singlet_carbene_count(self): if isinstance(v, Atom) and v.is_carbon() and v.lone_pairs > 0: carbenes += 1 return carbenes - + def get_radical_count(self): """ Return the total number of radical electrons on all atoms in the @@ -427,185 +246,28 @@ def get_radical_count(self): radicals += v.radical_electrons return radicals - def from_inchi(self, inchistr, backend='try-all'): - """ - Convert an InChI string `inchistr` to a molecular structure. - """ - import molecule.molecule.translator as translator - translator.from_inchi(self, inchistr, backend) - return self - - def from_smiles_like_string(self, smiles_like_string): - - smiles = smiles_like_string - - # input: smiles - # output: ind_ranger & cutting_label_list - ind_ranger , cutting_label_list = self.detect_cutting_label(smiles) - - smiles_replace_dict = {} - metal_list = ['[Na]', '[K]', '[Cs]', '[Fr]', '[Be]', '[Mg]', '[Ca]', '[Sr]', '[Ba]', '[Hf]', '[Nb]', '[Ta]', '[Db]', '[Mo]'] - for index, label_str in enumerate(cutting_label_list): - smiles_replace_dict[label_str] = metal_list[index] - - atom_replace_dict = {} - for key, value in smiles_replace_dict.items(): - atom_replace_dict[value] = key - - # replace cutting labels with elements in smiles - # to generate rdkit compatible SMILES - new_smi = self.replace_cutting_label(smiles, ind_ranger, cutting_label_list, smiles_replace_dict) - - from rdkit import Chem - rdkitmol = Chem.MolFromSmiles(new_smi) - - self.from_rdkit_mol(rdkitmol, atom_replace_dict) - - return self - - def detect_cutting_label(self, smiles): - - import re - from molecule.molecule.element import element_list - # store elements' symbol - all_element_list = [] - for element in element_list[1:]: - all_element_list.append(element.symbol) - - # store the tuple of matched indexes, however, - # the index might contain redundant elements such as C, Ra, (), Li, ... - index_indicator = [x.span() for x in re.finditer(r'(\w?[LR][^:()]?)', smiles)] - possible_cutting_label_list = re.findall(r'(\w?[LR][^:()]?)', smiles) - - cutting_label_list = [] - ind_ranger = [] - - # check if the matched items are cutting labels indeed - for i, strs in enumerate(possible_cutting_label_list): - # initialize "add" for every possible cutting label - add = False - if len(strs) == 1: - # it should be a cutting label either R or L - # add it to cutting label list - add = True - # add the index span - new_index_ranger = index_indicator[i] - elif len(strs)==2: - # it's possible to be L+digit, L+C, C+L, R+a - # check if it is a metal, if yes then don't add to cutting_label_list - if strs in all_element_list: - # do not add it to cutting label list - add = False - else: - add = True - # keep digit and remove the other non-metalic elements such as C - if strs[0] in ['L','R'] and strs[1].isdigit() == True: - # keep strs as it is - strs = strs - new_index_ranger = index_indicator[i] - elif strs[0] in ['L','R'] and strs[1].isdigit() != True: - strs = strs[0] - # keep the first index but subtract 1 for the end index - ind_tup = index_indicator[i] - int_ind = ind_tup[0] - end_ind = ind_tup[1]-1 - new_index_ranger = (int_ind,end_ind) - else: - strs = strs[1] - # add 1 for the start index and keep the end index - ind_tup = index_indicator[i] - int_ind = ind_tup[0]+1 - end_ind = ind_tup[1] - new_index_ranger = (int_ind,end_ind) - elif len(strs)==3: - # it's possible to be C+R+digit, C+L+i(metal), C+R+a(metal) - # only C+R+digit has cutting label - if strs[2].isdigit() == True: - add = True - strs = strs.replace(strs[0],"") - # add 1 for the start index and keep the end index - ind_tup = index_indicator[i] - int_ind = ind_tup[0]+1 - end_ind = ind_tup[1] - new_index_ranger = (int_ind,end_ind) - else: - # do not add this element to cutting_label_list - add = False - if add == True: - cutting_label_list.append(strs) - ind_ranger.append(new_index_ranger) - return ind_ranger, cutting_label_list - - def replace_cutting_label(self, smiles, ind_ranger, cutting_label_list, smiles_replace_dict): - - last_end_ind = 0 - new_smi = "" - - for ind, label_str in enumerate(cutting_label_list): - tup = ind_ranger[ind] - int_ind = tup[0] - end_ind = tup[1] - - element = smiles_replace_dict[label_str] - - if ind == len(cutting_label_list)-1: - new_smi = new_smi + smiles[last_end_ind:int_ind] + element + smiles[end_ind:] - else: - new_smi = new_smi + smiles[last_end_ind:int_ind] + element - - last_end_ind = end_ind - # if the smiles does not include cutting label - if new_smi == "": - return smiles - return new_smi - - def is_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False, strict=True): - """ - Returns :data:`True` if two graphs are isomorphic and :data:`False` - otherwise. The `initial_map` attribute can be used to specify a required - mapping from `self` to `other` (i.e. the atoms of `self` are the keys, - while the atoms of `other` are the values). The `other` parameter must - be a :class:`Graph` object, or a :class:`TypeError` is raised. - Also ensures multiplicities are also equal. - - Args: - initial_map (dict, optional): initial atom mapping to use - generate_initial_map (bool, optional): if ``True``, initialize map by pairing atoms with same labels - save_order (bool, optional): if ``True``, reset atom order after performing atom isomorphism - strict (bool, optional): if ``False``, perform isomorphism ignoring electrons - """ - # It only makes sense to compare a Molecule to a Molecule for full - # isomorphism, so raise an exception if this is not what was requested - if not isinstance(other, Graph): - raise TypeError('Got a {0} object for parameter "other", when a Molecule object is required.'.format(other.__class__)) - # Do the quick isomorphism comparison using the fingerprint - # Two fingerprint strings matching is a necessary (but not - # sufficient!) condition for the associated molecules to be isomorphic - if self.fingerprint != other.fingerprint: - return False - # check multiplicity - if self.multiplicity != other.multiplicity: - return False - - # Do the full isomorphism comparison - result = Graph.is_isomorphic(self, other, initial_map, generate_initial_map, save_order=save_order, strict=strict) - return result - - def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False): + def is_subgraph_isomorphic( + self, other, initial_map=None, generate_initial_map=False, save_order=False + ): """ - Fragment's subgraph isomorphism check is done by first creating + Fragment's subgraph isomorphism check is done by first creating a representative molecule of fragment, and then following same procedure of subgraph isomorphism check of `Molecule` object aganist `Group` object """ if not isinstance(other, gr.Group): - raise TypeError('Got a {0} object for parameter "other", when a Molecule object is required.'.format(other.__class__)) + raise TypeError( + 'Got a {0} object for parameter "other", when a Molecule object is required.'.format( + other.__class__ + ) + ) group = other mapping = self.assign_representative_molecule() # Check multiplicity if group.multiplicity: - if self.mol_repr.multiplicity not in group.multiplicity: return False + if self.mol_repr.multiplicity not in group.multiplicity: + return False # Compare radical counts if self.mol_repr.get_radical_count() < group.radicalCount: @@ -624,7 +286,7 @@ def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=F atms = [] initial_map = dict() for atom in self.atoms: - if atom.label and atom.label != '': + if atom.label and atom.label != "": atom_label_map = [a for a in other.atoms if a.label == atom.label] if not atom_label_map: return False @@ -638,10 +300,13 @@ def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=F # skip entries that map multiple graph atoms to the same subgraph atom if len(set(atmlist)) != len(atmlist): continue - for i,key in enumerate(keys): + for i, key in enumerate(keys): initial_map[key] = atmlist[i] - if self.is_mapping_valid(other, initial_map, equivalent=False) and \ - Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order): + if self.is_mapping_valid( + other, initial_map, equivalent=False + ) and Graph.is_subgraph_isomorphic( + self, other, initial_map, save_order=save_order + ): return True else: return False @@ -660,424 +325,61 @@ def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=F result = Graph.is_subgraph_isomorphic(self.mol_repr, other, new_initial_map) return result - def is_atom_in_cycle(self, atom): + def calculate_cp0(self): """ - Returns ``True`` if ``atom`` is in one or more cycles in the structure, ``False`` otherwise. + Return the value of the heat capacity at zero temperature in J/mol*K. """ - return self.is_vertex_in_cycle(atom) + self.assign_representative_molecule() + # currently linear fragment will be treated as non-linear molecule + return self.mol_repr.calculate_cp0() - def is_bond_in_cycle(self, bond): + def calculate_cpinf(self): """ - Returns ``True`` if the bond between atoms ``atom1`` and ``atom2`` - is in one or more cycles in the graph, ``False`` otherwise. + Return the value of the heat capacity at infinite temperature in J/mol*K. """ - return self.is_edge_in_cycle(bond) - - def assign_representative_molecule(self): + self.assign_representative_molecule() + # currently linear fragment will be treated as non-linear molecule + return self.mol_repr.calculate_cpinf() - # create a molecule from fragment.vertices.copy - mapping = self.copy_and_map() + def calculate_symmetry_number(self): + """ + Return the symmetry number for the structure. The symmetry number + includes both external and internal modes. First replace Cuttinglabel + with different elements and then calculate symmetry number + """ + from molecule.molecule.symmetry import calculate_symmetry_number - # replace CuttingLabel with C14 structure to avoid fragment symmetry number issue - atoms = [] - additional_atoms = [] - additional_bonds = [] - for vertex in self.vertices: + smiles = self.to_smiles() - mapped_vertex = mapping[vertex] - if isinstance(mapped_vertex, CuttingLabel): + _, cutting_label_list = self.detect_cutting_label(smiles) + + metal_list = [ + "[Cl]", + "[I]", + "[Si]", + "[F]", + "[Si+]", + "[Si-]", + "[Br]", + "[He+]", + "[Ne+]", + "[Ar+]", + "[He-]", + "[Ne-]", + "[Ar-]", + "[P]", + "[P+]", + "[P-]", + ] - # replace cutting label with atom C - atom_C1 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) + for index, element in enumerate(cutting_label_list): + smiles = smiles.replace(element, metal_list[index], 1) - for bondedAtom, bond in mapped_vertex.edges.items(): - new_bond = Bond(bondedAtom, atom_C1, order=bond.order) - - bondedAtom.edges[atom_C1] = new_bond - del bondedAtom.edges[mapped_vertex] + frag_sym = Molecule().from_smiles(smiles) - atom_C1.edges[bondedAtom] = new_bond - - # add hydrogens and carbon to make it CC - atom_H1 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H2 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C2 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H3 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H4 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C3 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H5 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H6 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C4 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C5 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H7 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H8 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H9 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C6 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H10 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C7 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H11 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H12 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C8 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H13 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C9 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H14 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H15 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H16 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C10 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H17 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C11 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H18 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H19 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H20 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C12 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H21 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C13 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H22 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_C14 = Atom(element=get_element('C'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H23 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H24 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atom_H25 = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - atoms.append(atom_C1) - - additional_atoms.extend([atom_H1, - atom_H2, - atom_H3, - atom_H4, - atom_H5, - atom_H6, - atom_H7, - atom_H8, - atom_H9, - atom_H10, - atom_H11, - atom_H12, - atom_H13, - atom_H14, - atom_H15, - atom_H16, - atom_H17, - atom_H18, - atom_H19, - atom_H20, - atom_H21, - atom_H22, - atom_H23, - atom_H24, - atom_H25, - atom_C2, - atom_C3, - atom_C4, - atom_C5, - atom_C6, - atom_C7, - atom_C8, - atom_C9, - atom_C10, - atom_C11, - atom_C12, - atom_C13, - atom_C14, - ]) - - additional_bonds.extend([Bond(atom_C1, atom_H1, 1), - Bond(atom_C1, atom_H2, 1), - Bond(atom_C2, atom_H3, 1), - Bond(atom_C2, atom_H4, 1), - Bond(atom_C1, atom_C2, 1), - Bond(atom_C2, atom_C3, 1), - Bond(atom_C3, atom_H5, 1), - Bond(atom_C3, atom_H6, 1), - Bond(atom_C3, atom_C4, 1), - Bond(atom_C4, atom_C5, 1), - Bond(atom_C5, atom_H7, 1), - Bond(atom_C5, atom_H8, 1), - Bond(atom_C5, atom_H9, 1), - Bond(atom_C4, atom_C6, 1), - Bond(atom_C6, atom_C7, 2), - Bond(atom_C6, atom_H10, 1), - Bond(atom_C7, atom_H11, 1), - Bond(atom_C7, atom_H12, 1), - Bond(atom_C4, atom_C8, 1), - Bond(atom_C8, atom_H13, 1), - Bond(atom_C8, atom_C9, 1), - Bond(atom_C9, atom_H14, 1), - Bond(atom_C9, atom_H15, 1), - Bond(atom_C9, atom_H16, 1), - Bond(atom_C8, atom_C10, 1), - Bond(atom_C10, atom_H17, 1), - Bond(atom_C10, atom_C11, 1), - Bond(atom_C11, atom_H18, 1), - Bond(atom_C11, atom_H19, 1), - Bond(atom_C11, atom_H20, 1), - Bond(atom_C10, atom_C12, 1), - Bond(atom_C12, atom_H21, 1), - Bond(atom_C12, atom_C13, 2), - Bond(atom_C13, atom_H22, 1), - Bond(atom_C13, atom_C14, 1), - Bond(atom_C14, atom_H23, 1), - Bond(atom_C14, atom_H24, 1), - Bond(atom_C14, atom_H25, 1), - ]) - - else: - atoms.append(mapped_vertex) - - mol_repr = Molecule() - mol_repr.atoms = atoms - for atom in additional_atoms: mol_repr.add_atom(atom) - for bond in additional_bonds: mol_repr.add_bond(bond) - # update connectivity - mol_repr.update() - - # create a species object from molecule - self.mol_repr = mol_repr - - return mapping - - def assign_representative_species(self): - - from molecule.species import Species - self.assign_representative_molecule() - self.species_repr = Species(molecule=[self.mol_repr]) - self.symmetry_number = self.get_symmetry_number() - self.species_repr.symmetry_number = self.symmetry_number - - def get_molecular_weight(self): - """ - Return the fragmental weight of the fragment in kg/mol. - """ - mass = 0 - for vertex in self.vertices: - if isinstance(vertex, Atom): - mass += vertex.element.mass - return mass - - def count_internal_rotors(self): - """ - Determine the number of internal rotors in the structure. Any single - bond not in a cycle and between two atoms that also have other bonds - is considered to be a pivot of an internal rotor. - """ - count = 0 - for atom1 in self.vertices: - for atom2, bond in atom1.edges.items(): - if (self.vertices.index(atom1) < self.vertices.index(atom2) and - bond.is_single() and not self.is_bond_in_cycle(bond)): - if len(atom1.edges) > 1 and len(atom2.edges) > 1: - count += 1 - return count - - def is_bond_in_cycle(self, bond): - """ - Return :data:``True`` if the bond between atoms ``atom1`` and ``atom2`` - is in one or more cycles in the graph, or :data:``False`` if not. - """ - return self.is_edge_in_cycle(bond) - - def calculate_cp0(self): - """ - Return the value of the heat capacity at zero temperature in J/mol*K. - """ - self.assign_representative_molecule() - # currently linear fragment will be treated as non-linear molecule - return self.mol_repr.calculate_cp0() - - def calculate_cpinf(self): - """ - Return the value of the heat capacity at infinite temperature in J/mol*K. - """ - self.assign_representative_molecule() - # currently linear fragment will be treated as non-linear molecule - return self.mol_repr.calculate_cpinf() - - def get_symmetry_number(self): - """ - Returns the symmetry number of Fragment. - First checks whether the value is stored as an attribute of Fragment. - If not, it calls the calculateSymmetryNumber method. - """ - if self.symmetry_number == -1: - self.calculate_symmetry_number() - return self.symmetry_number - - def calculate_symmetry_number(self): - """ - Return the symmetry number for the structure. The symmetry number - includes both external and internal modes. First replace Cuttinglabel - with different elements and then calculate symmetry number - """ - import re - from molecule.molecule.symmetry import calculate_symmetry_number - - smiles = self.to_smiles() - - _ , cutting_label_list = self.detect_cutting_label(smiles) - - metal_list = ['[Cl]', '[I]', '[Si]', '[F]', '[Si+]', '[Si-]', '[Br]', '[He+]', '[Ne+]', '[Ar+]', '[He-]', '[Ne-]', '[Ar-]', '[P]', '[P+]', '[P-]'] - - for index, element in enumerate(cutting_label_list): - smiles = smiles.replace(element, metal_list[index], 1) - - frag_sym = Molecule().from_smiles(smiles) - - frag_sym.update_connectivity_values() # for consistent results - self.symmetry_number = calculate_symmetry_number(frag_sym) - return self.symmetry_number + frag_sym.update_connectivity_values() # for consistent results + self.symmetry_number = calculate_symmetry_number(frag_sym) + return self.symmetry_number def is_radical(self): """ @@ -1089,10 +391,10 @@ def is_radical(self): return True return False - def update(self, sort_atoms=True): + def update(self, log_species=False, sort_atoms=False, raise_atomtype_exception=False): # currently sort_atoms does not work for fragments for v in self.vertices: - if isinstance(v, Atom): + if not isinstance(v, CuttingLabel): v.update_charge() self.update_atomtypes() @@ -1104,35 +406,30 @@ def update_atomtypes(self, log_species=True, raise_exception=True): Iterate through the atoms in the structure, checking their atom types to ensure they are correct (i.e. accurately describe their local bond environment) and complete (i.e. are as detailed as possible). - + If `raise_exception` is `False`, then the generic atomType 'R' will be prescribed to any atom when get_atomtype fails. Currently used for resonance hybrid atom types. """ - #Because we use lonepairs to match atomtypes and default is -100 when unspecified, - #we should update before getting the atomtype. + # Because we use lonepairs to match atomtypes and default is -100 when unspecified, + # we should update before getting the atomtype. self.update_lone_pairs() for v in self.vertices: - if not isinstance(v, Atom): continue + if isinstance(v, CuttingLabel): + continue try: v.atomtype = get_atomtype(v, v.edges) except AtomTypeError: if log_species: - logging.error("Could not update atomtypes for this fragment:\n{0}".format(self.to_adjacency_list())) + logging.error( + "Could not update atomtypes for this fragment:\n{0}".format( + self.to_adjacency_list() + ) + ) if raise_exception: raise - v.atomtype = ATOMTYPES['R'] - - def update_multiplicity(self): - """ - Update the multiplicity of a newly formed molecule. - """ - # Assume this is always true - # There are cases where 2 radicalElectrons is a singlet, but - # the triplet is often more stable, - self.multiplicity = self.get_radical_count() + 1 - + v.atomtype = ATOMTYPES["R"] def update_lone_pairs(self): """ @@ -1140,12 +437,22 @@ def update_lone_pairs(self): number of lone electron pairs, assuming a neutral molecule. """ for v in self.vertices: - if not isinstance(v, Atom): continue + if isinstance(v, CuttingLabel): + continue if not v.is_hydrogen(): order = v.get_total_bond_order() - v.lone_pairs = (elements.PeriodicSystem.valence_electrons[v.symbol] - v.radical_electrons - v.charge - int(order)) / 2.0 + v.lone_pairs = ( + elements.PeriodicSystem.valence_electrons[v.symbol] + - v.radical_electrons + - v.charge + - int(order) + ) / 2.0 if v.lone_pairs % 1 > 0 or v.lone_pairs > 4: - logging.error("Unable to determine the number of lone pairs for element {0} in {1}".format(v,self)) + logging.error( + "Unable to determine the number of lone pairs for element {0} in {1}".format( + v, self + ) + ) else: v.lone_pairs = 0 @@ -1153,77 +460,35 @@ def get_formula(self): """ Return the molecular formula for the fragment. """ - + # Count the number of each element in the molecule elements = {} cuttinglabels = {} for atom in self.vertices: symbol = atom.symbol elements[symbol] = elements.get(symbol, 0) + 1 - + # Use the Hill system to generate the formula - formula = '' - + formula = "" + # Carbon and hydrogen always come first if carbon is present - if 'C' in elements.keys(): - count = elements['C'] - formula += 'C{0:d}'.format(count) if count > 1 else 'C' - del elements['C'] - if 'H' in elements.keys(): - count = elements['H'] - formula += 'H{0:d}'.format(count) if count > 1 else 'H' - del elements['H'] + if "C" in elements.keys(): + count = elements["C"] + formula += "C{0:d}".format(count) if count > 1 else "C" + del elements["C"] + if "H" in elements.keys(): + count = elements["H"] + formula += "H{0:d}".format(count) if count > 1 else "H" + del elements["H"] # Other atoms are in alphabetical order # (This includes hydrogen if carbon is not present) keys = sorted(elements.keys()) for key in keys: count = elements[key] - formula += '{0}{1:d}'.format(key, count) if count > 1 else key - - return formula - - def get_representative_molecule(self, mode='minimal', update=True): - - if mode == 'minimal': - # create a molecule from fragment.vertices.copy - mapping = self.copy_and_map() - - # replace CuttingLabel with H - atoms = [] - for vertex in self.vertices: - - mapped_vertex = mapping[vertex] - if isinstance(mapped_vertex, CuttingLabel): - - # replace cutting label with atom H - atom_H = Atom(element=get_element('H'), - radical_electrons=0, - charge=0, - lone_pairs=0) - - for bondedAtom, bond in mapped_vertex.edges.items(): - new_bond = Bond(bondedAtom, atom_H, order=bond.order) - - bondedAtom.edges[atom_H] = new_bond - del bondedAtom.edges[mapped_vertex] + formula += "{0}{1:d}".format(key, count) if count > 1 else key - atom_H.edges[bondedAtom] = new_bond - - mapping[vertex] = atom_H - atoms.append(atom_H) - - else: - atoms.append(mapped_vertex) - - # Note: mapping is a dict with - # key: self.vertex and value: mol_repr.atom - mol_repr = Molecule() - mol_repr.atoms = atoms - if update: - mol_repr.update() - - return mol_repr, mapping + return formula def to_rdkit_mol(self, remove_h=False, return_mapping=True, save_order=False): """ @@ -1235,13 +500,15 @@ def to_rdkit_mol(self, remove_h=False, return_mapping=True, save_order=False): # so do not allow removeHs to be True raise "Currently fragment to_rdkit_mol only allows keeping all the hydrogens." - mol0, mapping = self.get_representative_molecule('minimal', update=False) + mol0, mapping = self.get_representative_molecule("minimal", update=False) - rdmol, rdAtomIdx_mol0 = converter.to_rdkit_mol(mol0, - remove_h=remove_h, - return_mapping=return_mapping, - sanitize=True, - save_order=save_order) + rdmol, rdAtomIdx_mol0 = converter.to_rdkit_mol( + mol0, + remove_h=remove_h, + return_mapping=return_mapping, + sanitize=True, + save_order=save_order, + ) rdAtomIdx_frag = {} for frag_atom, mol0_atom in mapping.items(): @@ -1261,53 +528,59 @@ def to_rdkit_mol(self, remove_h=False, return_mapping=True, save_order=False): idx = mol0.atoms.index(a) vertices_order.append((v, idx)) - adapted_vertices = [tup[0] for tup in sorted(vertices_order, key=lambda tup: tup[1])] + adapted_vertices = [ + tup[0] for tup in sorted(vertices_order, key=lambda tup: tup[1]) + ] self.vertices = adapted_vertices return rdmol, rdAtomIdx_frag - def to_adjacency_list(self, - label='', - remove_h=False, - remove_lone_pairs=False, - old_style=False): - """ - Convert the molecular structure to a string adjacency list. - """ - from molecule.molecule.adjlist import to_adjacency_list - result = to_adjacency_list(self.vertices, - self.multiplicity, - label=label, - group=False, - remove_h=remove_h, - remove_lone_pairs=remove_lone_pairs, - old_style=old_style) - return result - - def from_adjacency_list(self, adjlist, saturate_h=False, raise_atomtype_exception=True, - raise_charge_exception=True): + def from_adjacency_list( + self, + adjlist, + saturate_h=False, + raise_atomtype_exception=True, + raise_charge_exception=True, + ): """ Convert a string adjacency list `adjlist` to a fragment structure. Skips the first line (assuming it's a label) unless `withLabel` is ``False``. """ from molecule.molecule.adjlist import from_adjacency_list - - self.vertices, self.multiplicity, self.site, self.morphology = from_adjacency_list(adjlist, group=False, saturate_h=saturate_h) + + ( + self.vertices, + self.multiplicity, + self.site, + self.morphology, + ) = from_adjacency_list(adjlist, group=False, saturate_h=saturate_h) self.update_atomtypes(raise_exception=raise_atomtype_exception) - + # Check if multiplicity is possible - n_rad = self.get_radical_count() + n_rad = self.get_radical_count() multiplicity = self.multiplicity - if not (n_rad + 1 == multiplicity or n_rad - 1 == multiplicity or - n_rad - 3 == multiplicity or n_rad - 5 == multiplicity): - raise ValueError('Impossible multiplicity for molecule\n{0}\n multiplicity = {1} and number of ' - 'unpaired electrons = {2}'.format(self.to_adjacency_list(), multiplicity, n_rad)) + if not ( + n_rad + 1 == multiplicity + or n_rad - 1 == multiplicity + or n_rad - 3 == multiplicity + or n_rad - 5 == multiplicity + ): + raise ValueError( + "Impossible multiplicity for molecule\n{0}\n multiplicity = {1} and number of " + "unpaired electrons = {2}".format( + self.to_adjacency_list(), multiplicity, n_rad + ) + ) if raise_charge_exception: if self.get_net_charge() != 0: - raise ValueError('Non-neutral molecule encountered. ' - 'Currently, AFM does not support ion chemistry.\n {0}'.format(adjlist)) + raise ValueError( + "Non-neutral molecule encountered. " + "Currently, AFM does not support ion chemistry.\n {0}".format( + adjlist + ) + ) return self def get_aromatic_rings(self, rings=None, save_order=False): @@ -1323,6 +596,7 @@ def get_aromatic_rings(self, rings=None, save_order=False): """ from rdkit.Chem.rdchem import BondType + AROMATIC = BondType.AROMATIC if rings is None: @@ -1332,9 +606,11 @@ def get_aromatic_rings(self, rings=None, save_order=False): return [], [] try: - rdkitmol, rdAtomIndices = self.to_rdkit_mol(remove_h=False, return_mapping=True, save_order=save_order) + rdkitmol, rdAtomIndices = self.to_rdkit_mol( + remove_h=False, return_mapping=True, save_order=save_order + ) except ValueError: - logging.warning('Unable to check aromaticity by converting to RDKit Mol.') + logging.warning("Unable to check aromaticity by converting to RDKit Mol.") else: aromatic_rings = [] aromatic_bonds = [] @@ -1345,11 +621,17 @@ def get_aromatic_rings(self, rings=None, save_order=False): if not atom1.is_carbon(): # all atoms in the ring must be carbon in RMG for our definition of aromatic break - for atom2 in ring0[i + 1:]: + for atom2 in ring0[i + 1 :]: if self.has_bond(atom1, atom2): - if rdkitmol.GetBondBetweenAtoms(rdAtomIndices[atom1], - rdAtomIndices[atom2]).GetBondType() is AROMATIC: - aromatic_bonds_in_ring.append(self.get_bond(atom1, atom2)) + if ( + rdkitmol.GetBondBetweenAtoms( + rdAtomIndices[atom1], rdAtomIndices[atom2] + ).GetBondType() + is AROMATIC + ): + aromatic_bonds_in_ring.append( + self.get_bond(atom1, atom2) + ) else: # didn't break so all atoms are carbon if len(aromatic_bonds_in_ring) == 6: aromatic_rings.append(ring0) @@ -1358,85 +640,13 @@ def get_aromatic_rings(self, rings=None, save_order=False): return aromatic_rings, aromatic_bonds def is_aromatic(self): - """ - Returns ``True`` if the fragment is aromatic, or ``False`` if not. """ - mol0, _ = self.get_representative_molecule('minimal') - return mol0.is_aromatic() - - def atom_ids_valid(self): - """ - Checks to see if the atom IDs are valid in this structure - """ - num_atoms = len(self.atoms) - num_IDs = len(set([atom.id for atom in self.atoms])) - - if num_atoms == num_IDs: - # all are unique - return True - return False - - def kekulize(self): - """ - Kekulizes an aromatic molecule. + Returns ``True`` if the fragment is aromatic, or ``False`` if not. """ - kekulize(self) - - def assign_atom_ids(self): - """ - Assigns an index to every atom in the fragment for tracking purposes. - Uses entire range of cython's integer values to reduce chance of duplicates - """ - - global atom_id_counter - - for atom in self.atoms: - atom.id = atom_id_counter - atom_id_counter += 1 - if atom_id_counter == 2**15: - atom_id_counter = -2**15 - - def generate_resonance_structures(self, keep_isomorphic=False, filter_structures=True, save_order=False): - """Returns a list of resonance structures of the fragment.""" - return resonance.generate_resonance_structures(self, keep_isomorphic=keep_isomorphic, - filter_structures = filter_structures, - save_order=save_order, - ) - - def is_identical(self, other, strict=True): - """ - Performs isomorphism checking, with the added constraint that atom IDs must match. - - Primary use case is tracking atoms in reactions for reaction degeneracy determination. - - Returns :data:`True` if two graphs are identical and :data:`False` otherwise. - """ - - if not isinstance(other, (Fragment, Molecule)): - raise TypeError('Got a {0} object for parameter "other", when a Fragment object is required.'.format(other.__class__)) - - # Get a set of atom indices for each molecule - atom_ids = set([atom.id for atom in self.atoms]) - other_ids = set([atom.id for atom in other.atoms]) - - if atom_ids == other_ids: - # If the two molecules have the same indices, then they might be identical - # Sort the atoms by ID - atom_list = sorted(self.atoms, key=lambda x: x.id) - other_list = sorted(other.atoms, key=lambda x: x.id) - - # If matching atom indices gives a valid mapping, then the molecules are fully identical - mapping = {} - for atom1, atom2 in zip(atom_list, other_list): - mapping[atom1] = atom2 - - return self.is_mapping_valid(other, mapping, equivalent=True, strict=strict) - else: - # The molecules don't have the same set of indices, so they are not identical - return False + mol0, _ = self.get_representative_molecule("minimal") + return mol0.is_aromatic() def to_smiles(self): - cutting_label_list = [] for vertex in self.vertices: if isinstance(vertex, CuttingLabel): @@ -1447,9 +657,10 @@ def to_smiles(self): for ind, atom in enumerate(smiles_before.atoms): element_symbol = atom.symbol if isinstance(atom, CuttingLabel): - substi = Atom(element=get_element('Si'), + substi = Atom( + element=get_element("Si"), radical_electrons=0, - charge=0, + charge=-3, lone_pairs=3) substi.label = element_symbol @@ -1471,7 +682,8 @@ def to_smiles(self): mol_repr.update() smiles_after = mol_repr.to_smiles() import re - smiles = re.sub(r'\[Si-3\]', '', smiles_after) + + smiles = re.sub(r"\[Si-3\]", "", smiles_after) return smiles @@ -1481,10 +693,11 @@ def get_element_count(self): """ element_count = {} for atom in self.vertices: - if not isinstance(atom, Atom): continue + if isinstance(atom, CuttingLabel): + continue symbol = atom.element.symbol isotope = atom.element.isotope - key = symbol # if isotope == -1 else (symbol, isotope) + key = symbol # if isotope == -1 else (symbol, isotope) if key in element_count: element_count[key] += 1 else: @@ -1492,69 +705,14 @@ def get_element_count(self): return element_count - def get_url(self): - """ - Get a URL to the fragment's info page on the RMG website. - """ - - base_url = "http://rmg.mit.edu/database/molecule/" - adjlist = self.to_adjacency_list(remove_h=False) - url = base_url + quote(adjlist) - return url.strip('_') - - def is_linear(self): - """ - Return :data:`True` if the structure is linear and :data:`False` - otherwise. - """ - - atom_count = len(self.vertices) - - # Monatomic molecules are definitely nonlinear - if atom_count == 1: - return False - # Diatomic molecules are definitely linear - elif atom_count == 2: - return True - # Cyclic molecules are definitely nonlinear - elif self.is_cyclic(): - return False - - # True if all bonds are double bonds (e.g. O=C=O) - all_double_bonds = True - for atom1 in self.vertices: - for bond in atom1.edges.values(): - if not bond.is_double(): all_double_bonds = False - if all_double_bonds: return True - - # True if alternating single-triple bonds (e.g. H-C#C-H) - # This test requires explicit hydrogen atoms - for atom in self.vertices: - bonds = list(atom.edges.values()) - if len(bonds)==1: - continue # ok, next atom - if len(bonds)>2: - break # fail! - if bonds[0].is_single() and bonds[1].is_triple(): - continue # ok, next atom - if bonds[1].is_single() and bonds[0].is_triple(): - continue # ok, next atom - break # fail if we haven't continued - else: - # didn't fail - return True - - # not returned yet? must be nonlinear - return False - def saturate_radicals(self): """ - Saturate the fragment by replacing all radicals with bonds to hydrogen atoms. Changes self molecule object. + Saturate the fragment by replacing all radicals with bonds to hydrogen atoms. Changes self molecule object. """ added = {} for atom in self.vertices: for i in range(atom.radical_electrons): - H = Atom('H', radical_electrons=0, lone_pairs=0, charge=0) + H = Atom("H", radical_electrons=0, lone_pairs=0, charge=0) bond = Bond(atom, H, 1) self.add_atom(H) self.add_bond(bond) @@ -1562,7 +720,7 @@ def saturate_radicals(self): added[atom] = [] added[atom].append([H, bond]) atom.decrement_radical() - + # Update the atom types of the saturated structure (not sure why # this is necessary, because saturating with H shouldn't be # changing atom types, but it doesn't hurt anything and is not @@ -1585,29 +743,32 @@ def is_aryl_radical(self, aromatic_rings=None, save_order=False): aromatic_rings = self.get_aromatic_rings(save_order=save_order)[0] total = self.get_radical_count() - aromatic_atoms = set([atom for atom in itertools.chain.from_iterable(aromatic_rings)]) + aromatic_atoms = set( + [atom for atom in itertools.chain.from_iterable(aromatic_rings)] + ) aryl = sum([atom.radical_electrons for atom in aromatic_atoms]) return total == aryl - def get_num_atoms(self, element = None): + def get_num_atoms(self, element=None): """ Return the number of atoms in molecule. If element is given, ie. "H" or "C", the number of atoms of that element is returned. """ num_atoms = 0 - if element == None: + if element is None: for vertex in self.vertices: - if isinstance(vertex, Atom): + if not isinstance(vertex, CuttingLabel): num_atoms += 1 else: for vertex in self.vertices: - if isinstance(vertex, Atom): + if not isinstance(vertex, CuttingLabel): if vertex.element.symbol == element: num_atoms += 1 return num_atoms - def from_rdkit_mol(self, rdkitmol, atom_replace_dict = None): + # extension methods + def from_rdkit_mol(self, rdkitmol, atom_replace_dict=None): """ Convert a RDKit Mol object `rdkitmol` to a molecular structure. Uses `RDKit `_ to perform the conversion. @@ -1636,12 +797,12 @@ def from_rdkit_mol(self, rdkitmol, atom_replace_dict = None): radical_electrons = rdkitatom.GetNumRadicalElectrons() ELE = element.symbol - if '[' + ELE + ']' in atom_replace_dict: - cutting_label_name = atom_replace_dict['[' + ELE + ']'] + if "[" + ELE + "]" in atom_replace_dict: + cutting_label_name = atom_replace_dict["[" + ELE + "]"] cutting_label = CuttingLabel(name=cutting_label_name) self.vertices.append(cutting_label) else: - atom = Atom(element, radical_electrons, charge, '', 0) + atom = Atom(element, radical_electrons, charge, "", 0) self.vertices.append(atom) # Add bonds by iterating again through atoms @@ -1652,13 +813,13 @@ def from_rdkit_mol(self, rdkitmol, atom_replace_dict = None): # Process bond type rdbondtype = rdkitbond.GetBondType() - if rdbondtype.name == 'SINGLE': + if rdbondtype.name == "SINGLE": order = 1 - elif rdbondtype.name == 'DOUBLE': + elif rdbondtype.name == "DOUBLE": order = 2 - elif rdbondtype.name == 'TRIPLE': + elif rdbondtype.name == "TRIPLE": order = 3 - elif rdbondtype.name == 'AROMATIC': + elif rdbondtype.name == "AROMATIC": order = 1.5 bond = Bond(self.vertices[i], self.vertices[j], order) @@ -1677,7 +838,7 @@ def from_rdkit_mol(self, rdkitmol, atom_replace_dict = None): return self - def cut_molecule(self, output_smiles = False, cut_through = True, size_threshold = None): + def cut_molecule(self, output_smiles=False, cut_through=True, size_threshold=None): """ For given input, output a list of cut fragments (either string or Fragment). if output_smiles = True, the output list of fragments will be smiles. @@ -1693,23 +854,33 @@ def cut_molecule(self, output_smiles = False, cut_through = True, size_threshold # slice mol frag_smiles_list = [] if cut_through: - arom_cut_frag = self.sliceitup_arom(mol.to_smiles(), size_threshold=size_threshold) + arom_cut_frag = self.sliceitup_arom( + mol.to_smiles(), size_threshold=size_threshold + ) for frag in arom_cut_frag: - aliph_cut_frag = self.sliceitup_aliph(frag, size_threshold=size_threshold) + aliph_cut_frag = self.sliceitup_aliph( + frag, size_threshold=size_threshold + ) for ele in aliph_cut_frag: frag_smiles_list.append(ele) else: # if aromatic, only perform sliceitup_arom, if aliphatic, only sliceitup_aliph - if mol.is_aromatic() == True: + if mol.is_aromatic(): # try aromatic cut first, if no cut found, try aliphatic cut then - frag_smiles_list = self.sliceitup_arom(mol.to_smiles(), size_threshold=size_threshold) + frag_smiles_list = self.sliceitup_arom( + mol.to_smiles(), size_threshold=size_threshold + ) if len(frag_smiles_list) == 1: # try aliphatic cut then - frag_smiles_list = self.sliceitup_aliph(mol.to_smiles(), size_threshold=size_threshold) + frag_smiles_list = self.sliceitup_aliph( + mol.to_smiles(), size_threshold=size_threshold + ) else: - frag_smiles_list = self.sliceitup_aliph(mol.to_smiles(), size_threshold=size_threshold) + frag_smiles_list = self.sliceitup_aliph( + mol.to_smiles(), size_threshold=size_threshold + ) - if output_smiles == True: + if output_smiles: return frag_smiles_list else: frag_list = [] @@ -1719,12 +890,12 @@ def cut_molecule(self, output_smiles = False, cut_through = True, size_threshold frag_list.append(res_frag) return frag_list - def sliceitup_arom(self, molecule, size_threshold = None): + def sliceitup_arom(self, molecule, size_threshold=None): """ Several specified aromatic patterns """ # set min size for each aliphatic fragment size - if size_threshold != None: + if size_threshold: size_threshold = size_threshold else: size_threshold = 5 @@ -1744,18 +915,20 @@ def sliceitup_arom(self, molecule, size_threshold = None): # replace CuttingLabel to special Atom (metal) in rdkit for atom, idx in rdAtomIdx_frag.items(): - if isinstance(atom,CuttingLabel): + if isinstance(atom, CuttingLabel): cuttinglabel_atom = molecule_to_cut.GetAtomWithIdx(idx) - if atom.symbol == 'R': - cuttinglabel_atom.SetAtomicNum(11) #[Na], will replace back to CuttingLabel later + if atom.symbol == "R": + cuttinglabel_atom.SetAtomicNum( + 11 + ) # [Na], will replace back to CuttingLabel later else: - cuttinglabel_atom.SetAtomicNum(19) #[K] + cuttinglabel_atom.SetAtomicNum(19) # [K] # substructure matching - pattern_list = ['pattern_1','pattern_2','pattern_3','pattern_4'] + pattern_list = ["pattern_1", "pattern_2", "pattern_3", "pattern_4"] frag_list = [] for pattern in pattern_list: - emol, atom_map_index = self.pattern_call('Arom', pattern) + emol, atom_map_index = self.pattern_call("Arom", pattern) # start pattern matching atom_map = molecule_to_cut.GetSubstructMatches(emol) if atom_map: @@ -1765,31 +938,39 @@ def sliceitup_arom(self, molecule, size_threshold = None): bonds_to_cut = [] for ind in atom_map_index: b1 = matched_atom_map[ind] - b2 = matched_atom_map[ind+1] + b2 = matched_atom_map[ind + 1] bond = molecule_to_cut.GetBondBetweenAtoms(b1, b2) bonds_to_cut.append(bond) # Break bonds newmol = Chem.RWMol(molecule_to_cut) # fragmentize - new_mol = Chem.FragmentOnBonds(newmol, [bond.GetIdx() for bond in bonds_to_cut], dummyLabels=[(0,0)]*len(bonds_to_cut)) + new_mol = Chem.FragmentOnBonds( + newmol, + [bond.GetIdx() for bond in bonds_to_cut], + dummyLabels=[(0, 0)] * len(bonds_to_cut), + ) # mol_set contains new set of fragments - mol_set = Chem.GetMolFrags(new_mol,asMols=True) + mol_set = Chem.GetMolFrags(new_mol, asMols=True) # check all fragments' size - if all( sum(1 for atom in mol.GetAtoms() if atom.GetAtomicNum() == 6) >= size_threshold for mol in mol_set): + if all( + sum(1 for atom in mol.GetAtoms() if atom.GetAtomicNum() == 6) + >= size_threshold + for mol in mol_set + ): # replace * at cutting position with cutting label - for ind,rdmol in enumerate(mol_set): - frag=Chem.MolToSmiles(rdmol) - if len(mol_set) > 2: # means it cut into 3 fragments - if frag.count('*') > 1: + for ind, rdmol in enumerate(mol_set): + frag = Chem.MolToSmiles(rdmol) + if len(mol_set) > 2: # means it cut into 3 fragments + if frag.count("*") > 1: # replace both with R - frag_smi = frag.replace('*','R') + frag_smi = frag.replace("*", "R") else: - frag_smi = frag.replace('*','L') - else: # means it only cut once, generate 2 fragments + frag_smi = frag.replace("*", "L") + else: # means it only cut once, generate 2 fragments if ind == 0: - frag_smi = frag.replace('*','R') + frag_smi = frag.replace("*", "R") else: - frag_smi = frag.replace('*','L') + frag_smi = frag.replace("*", "L") frag_list.append(frag_smi) break else: @@ -1812,35 +993,35 @@ def sliceitup_arom(self, molecule, size_threshold = None): frag_list_replaced = [] # replace metal atom back to Cuttinglabel for metal_frag in frag_list: - n_frag_smiles = metal_frag.replace('[Na]', 'R') - nn_frag_smiles = n_frag_smiles.replace('[K]', 'L') + n_frag_smiles = metal_frag.replace("[Na]", "R") + nn_frag_smiles = n_frag_smiles.replace("[K]", "L") frag_list_replaced.append(nn_frag_smiles) frag_list = frag_list_replaced if isinstance(molecule, str): frag_list_new = [] for frag in frag_list: - n_frag_smiles = frag.replace('[Na]', 'R') - new_frag_smiles = n_frag_smiles.replace('[K]', 'L') + n_frag_smiles = frag.replace("[Na]", "R") + new_frag_smiles = n_frag_smiles.replace("[K]", "L") frag_list_new.append(new_frag_smiles) return frag_list_new elif isinstance(molecule, Fragment): frag_list_new = [] for frag in frag_list: - n_frag_smiles = frag.replace('[Na]', 'R') - new_frag_smiles = n_frag_smiles.replace('[K]', 'L') + n_frag_smiles = frag.replace("[Na]", "R") + new_frag_smiles = n_frag_smiles.replace("[K]", "L") frag = Fragment().from_smiles_like_string(new_frag_smiles) res_frag = frag.generate_resonance_structures()[0] frag_list_new.append(res_frag) return frag_list_new - def sliceitup_aliph(self, molecule, size_threshold = None): + def sliceitup_aliph(self, molecule, size_threshold=None): """ Several specified aliphatic patterns """ # set min size for each aliphatic fragment size - if size_threshold != None: + if size_threshold: size_threshold = size_threshold else: size_threshold = 5 @@ -1860,18 +1041,20 @@ def sliceitup_aliph(self, molecule, size_threshold = None): # replace CuttingLabel to special Atom (metal) in rdkit for atom, idx in rdAtomIdx_frag.items(): - if isinstance(atom,CuttingLabel): + if isinstance(atom, CuttingLabel): cuttinglabel_atom = molecule_to_cut.GetAtomWithIdx(idx) - if atom.symbol == 'R': - cuttinglabel_atom.SetAtomicNum(11) #[Na], will replace back to CuttingLabel later + if atom.symbol == "R": + cuttinglabel_atom.SetAtomicNum( + 11 + ) # [Na], will replace back to CuttingLabel later else: - cuttinglabel_atom.SetAtomicNum(19) #[K] + cuttinglabel_atom.SetAtomicNum(19) # [K] # substructure matching - pattern_list = ['pattern_1','pattern_2','pattern_3'] + pattern_list = ["pattern_1", "pattern_2", "pattern_3"] frag_list = [] for pattern in pattern_list: - emol, atom_map_index = self.pattern_call('Aliph', pattern) + emol, atom_map_index = self.pattern_call("Aliph", pattern) # start pattern matching atom_map = molecule_to_cut.GetSubstructMatches(emol) if atom_map: @@ -1884,31 +1067,39 @@ def sliceitup_aliph(self, molecule, size_threshold = None): bonds_to_cut = [] for ind in atom_map_index: b1 = matched_atom_map[ind] - b2 = matched_atom_map[ind+1] + b2 = matched_atom_map[ind + 1] bond = molecule_to_cut.GetBondBetweenAtoms(b1, b2) bonds_to_cut.append(bond) # Break bonds newmol = Chem.RWMol(molecule_to_cut) # fragmentize - new_mol = Chem.FragmentOnBonds(newmol, [bond.GetIdx() for bond in bonds_to_cut], dummyLabels=[(0,0)]*len(bonds_to_cut)) + new_mol = Chem.FragmentOnBonds( + newmol, + [bond.GetIdx() for bond in bonds_to_cut], + dummyLabels=[(0, 0)] * len(bonds_to_cut), + ) # mol_set contains new set of fragments - mol_set = Chem.GetMolFrags(new_mol,asMols=True) + mol_set = Chem.GetMolFrags(new_mol, asMols=True) # check all fragments' size - if all( sum(1 for atom in mol.GetAtoms() if atom.GetAtomicNum() == 6) >= size_threshold for mol in mol_set): + if all( + sum(1 for atom in mol.GetAtoms() if atom.GetAtomicNum() == 6) + >= size_threshold + for mol in mol_set + ): # replace * at cutting position with cutting label - for ind,rdmol in enumerate(mol_set): - frag=Chem.MolToSmiles(rdmol) - if len(mol_set) > 2: # means it cut into 3 fragments - if frag.count('*') > 1: + for ind, rdmol in enumerate(mol_set): + frag = Chem.MolToSmiles(rdmol) + if len(mol_set) > 2: # means it cut into 3 fragments + if frag.count("*") > 1: # replace both with R - frag_smi = frag.replace('*','R') + frag_smi = frag.replace("*", "R") else: - frag_smi = frag.replace('*','L') - else: # means it only cut once, generate 2 fragments + frag_smi = frag.replace("*", "L") + else: # means it only cut once, generate 2 fragments if ind == 0: - frag_smi = frag.replace('*','R') + frag_smi = frag.replace("*", "R") else: - frag_smi = frag.replace('*','L') + frag_smi = frag.replace("*", "L") frag_list.append(frag_smi) break else: @@ -1931,23 +1122,23 @@ def sliceitup_aliph(self, molecule, size_threshold = None): frag_list_replaced = [] # replace metal atom back to Cuttinglabel for metal_frag in frag_list: - n_frag_smiles = metal_frag.replace('[Na]', 'R') - nn_frag_smiles = n_frag_smiles.replace('[K]', 'L') + n_frag_smiles = metal_frag.replace("[Na]", "R") + nn_frag_smiles = n_frag_smiles.replace("[K]", "L") frag_list_replaced.append(nn_frag_smiles) frag_list = frag_list_replaced if isinstance(molecule, str): frag_list_new = [] for frag in frag_list: - n_frag_smiles = frag.replace('[Na]', 'R') - new_frag_smiles = n_frag_smiles.replace('[K]', 'L') + n_frag_smiles = frag.replace("[Na]", "R") + new_frag_smiles = n_frag_smiles.replace("[K]", "L") frag_list_new.append(new_frag_smiles) return frag_list_new elif isinstance(molecule, Fragment): frag_list_new = [] for frag in frag_list: - n_frag_smiles = frag.replace('[Na]', 'R') - new_frag_smiles = n_frag_smiles.replace('[K]', 'L') + n_frag_smiles = frag.replace("[Na]", "R") + new_frag_smiles = n_frag_smiles.replace("[K]", "L") frag = Fragment().from_smiles_like_string(new_frag_smiles) res_frag = frag.generate_resonance_structures()[0] @@ -1959,79 +1150,79 @@ def pattern_call(self, pattern_type, pattern): pattern_type currently only supports 'Aliph' and 'Arom', specifying which pattern will give output as pattern in rdkit mol and atom_map_index for substructure matching """ - if pattern_type == 'Arom': - if pattern == 'pattern_1': - mol = Chem.MolFromSmiles('c1ccccc1C(C(c2ccccc2)CCCCC)CCCCC') + if pattern_type == "Arom": + if pattern == "pattern_1": + mol = Chem.MolFromSmiles("c1ccccc1C(C(c2ccccc2)CCCCC)CCCCC") emol = Chem.RWMol(mol) # add some H at terminal aliphatic C to avoid cutting at potential allylic C - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(16,24,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(16,25,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(21,26,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(21,27,Chem.rdchem.BondType.SINGLE) - atom_map_index = [16,21] - - if pattern == 'pattern_2': - mol = Chem.MolFromSmiles('c1ccccc1C(CCCCC)CCCCC') + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(16, 24, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(16, 25, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(21, 26, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(21, 27, Chem.rdchem.BondType.SINGLE) + atom_map_index = [16, 21] + + if pattern == "pattern_2": + mol = Chem.MolFromSmiles("c1ccccc1C(CCCCC)CCCCC") emol = Chem.RWMol(mol) # add some H at terminal aliphatic C to avoid cutting at potential allylic C - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(14,17,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(14,18,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(9,19,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(9,20,Chem.rdchem.BondType.SINGLE) - atom_map_index = [9,14] - - if pattern == 'pattern_3': - mol = Chem.MolFromSmiles('c1ccccc1C(=C)CCCCCC') + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(14, 17, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(14, 18, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(9, 19, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(9, 20, Chem.rdchem.BondType.SINGLE) + atom_map_index = [9, 14] + + if pattern == "pattern_3": + mol = Chem.MolFromSmiles("c1ccccc1C(=C)CCCCCC") emol = Chem.RWMol(mol) # add some H at terminal aliphatic C to avoid cutting at potential allylic C - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(11,14,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(11,15,Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(11, 14, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(11, 15, Chem.rdchem.BondType.SINGLE) atom_map_index = [11] - if pattern == 'pattern_4': - mol = Chem.MolFromSmiles('c1ccccc1CCCCCC') + if pattern == "pattern_4": + mol = Chem.MolFromSmiles("c1ccccc1CCCCCC") emol = Chem.RWMol(mol) # add some H at terminal aliphatic C to avoid cutting at potential allylic C - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(9,12,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(9,13,Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(9, 12, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(9, 13, Chem.rdchem.BondType.SINGLE) atom_map_index = [9] - elif pattern_type == 'Aliph': - if pattern == 'pattern_1': - mol = Chem.MolFromSmiles('CCC=CCCCCC') + elif pattern_type == "Aliph": + if pattern == "pattern_1": + mol = Chem.MolFromSmiles("CCC=CCCCCC") emol = Chem.RWMol(mol) # add some H at terminal aliphatic C to avoid cutting at potential allylic C - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(6,9,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(6,10,Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(6, 9, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(6, 10, Chem.rdchem.BondType.SINGLE) atom_map_index = [6] - if pattern == 'pattern_2': - mol = Chem.MolFromSmiles('C=CCCCCC') + if pattern == "pattern_2": + mol = Chem.MolFromSmiles("C=CCCCCC") emol = Chem.RWMol(mol) # add some H at terminal aliphatic C to avoid cutting at potential allylic C - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(4,7,Chem.rdchem.BondType.SINGLE) - emol.AddAtom(Chem.rdchem.Atom('H')) - emol.AddBond(4,8,Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(4, 7, Chem.rdchem.BondType.SINGLE) + emol.AddAtom(Chem.rdchem.Atom("H")) + emol.AddBond(4, 8, Chem.rdchem.BondType.SINGLE) atom_map_index = [4] - if pattern == 'pattern_3': - mol = Chem.MolFromSmiles('CCCCCC') - mol = Chem.AddHs(mol,onlyOnAtoms=[2,3]) + if pattern == "pattern_3": + mol = Chem.MolFromSmiles("CCCCCC") + mol = Chem.AddHs(mol, onlyOnAtoms=[2, 3]) emol = Chem.RWMol(mol) atom_map_index = [2] else: @@ -2049,6 +1240,610 @@ def check_in_ring(self, rd_mol, mapped_atom_idx): return True return False -# this variable is used to name atom IDs so that there are as few conflicts by -# using the entire space of integer objects -atom_id_counter = -2**15 + def is_multidentate(self): + """ + Return ``True`` if the adsorbate contains at least two binding sites, + or ``False`` otherwise. + """ + surface_sites = 0 + for atom in self.vertices: + if atom.is_surface_site(): + surface_sites += 1 + if surface_sites >= 2: + return True + return False + + def from_smiles_like_string(self, smiles_like_string): + smiles = smiles_like_string + + # input: smiles + # output: ind_ranger & cutting_label_list + ind_ranger, cutting_label_list = self.detect_cutting_label(smiles) + + smiles_replace_dict = {} + metal_list = [ + "[Na]", + "[K]", + "[Cs]", + "[Fr]", + "[Be]", + "[Mg]", + "[Ca]", + "[Sr]", + "[Ba]", + "[Hf]", + "[Nb]", + "[Ta]", + "[Db]", + "[Mo]", + ] + for index, label_str in enumerate(cutting_label_list): + smiles_replace_dict[label_str] = metal_list[index] + + atom_replace_dict = {} + for key, value in smiles_replace_dict.items(): + atom_replace_dict[value] = key + + # replace cutting labels with elements in smiles + # to generate rdkit compatible SMILES + new_smi = self.replace_cutting_label( + smiles, ind_ranger, cutting_label_list, smiles_replace_dict + ) + + from rdkit import Chem + + rdkitmol = Chem.MolFromSmiles(new_smi) + + self.from_rdkit_mol(rdkitmol, atom_replace_dict) + + return self + + def detect_cutting_label(self, smiles): + import re + from molecule.molecule.element import element_list + + # store elements' symbol + all_element_list = [] + for element in element_list[1:]: + all_element_list.append(element.symbol) + + # store the tuple of matched indexes, however, + # the index might contain redundant elements such as C, Ra, (), Li, ... + index_indicator = [x.span() for x in re.finditer(r"(\w?[LR][^:()]?)", smiles)] + possible_cutting_label_list = re.findall(r"(\w?[LR][^:()]?)", smiles) + + cutting_label_list = [] + ind_ranger = [] + + # check if the matched items are cutting labels indeed + for i, strs in enumerate(possible_cutting_label_list): + # initialize "add" for every possible cutting label + add = False + if len(strs) == 1: + # it should be a cutting label either R or L + # add it to cutting label list + add = True + # add the index span + new_index_ranger = index_indicator[i] + elif len(strs) == 2: + # it's possible to be L+digit, L+C, C+L, R+a + # check if it is a metal, if yes then don't add to cutting_label_list + if strs in all_element_list: + # do not add it to cutting label list + add = False + else: + add = True + # keep digit and remove the other non-metalic elements such as C + if strs[0] in ["L", "R"] and strs[1].isdigit(): + # keep strs as it is + strs = strs + new_index_ranger = index_indicator[i] + elif strs[0] in ["L", "R"] and not strs[1].isdigit(): + strs = strs[0] + # keep the first index but subtract 1 for the end index + ind_tup = index_indicator[i] + int_ind = ind_tup[0] + end_ind = ind_tup[1] - 1 + new_index_ranger = (int_ind, end_ind) + else: + strs = strs[1] + # add 1 for the start index and keep the end index + ind_tup = index_indicator[i] + int_ind = ind_tup[0] + 1 + end_ind = ind_tup[1] + new_index_ranger = (int_ind, end_ind) + elif len(strs) == 3: + # it's possible to be C+R+digit, C+L+i(metal), C+R+a(metal) + # only C+R+digit has cutting label + if strs[2].isdigit(): + add = True + strs = strs.replace(strs[0], "") + # add 1 for the start index and keep the end index + ind_tup = index_indicator[i] + int_ind = ind_tup[0] + 1 + end_ind = ind_tup[1] + new_index_ranger = (int_ind, end_ind) + else: + # do not add this element to cutting_label_list + add = False + if add: + cutting_label_list.append(strs) + ind_ranger.append(new_index_ranger) + return ind_ranger, cutting_label_list + + def replace_cutting_label( + self, smiles, ind_ranger, cutting_label_list, smiles_replace_dict + ): + last_end_ind = 0 + new_smi = "" + + for ind, label_str in enumerate(cutting_label_list): + tup = ind_ranger[ind] + int_ind = tup[0] + end_ind = tup[1] + + element = smiles_replace_dict[label_str] + + if ind == len(cutting_label_list) - 1: + new_smi = ( + new_smi + smiles[last_end_ind:int_ind] + element + smiles[end_ind:] + ) + else: + new_smi = new_smi + smiles[last_end_ind:int_ind] + element + + last_end_ind = end_ind + # if the smiles does not include cutting label + if new_smi == "": + return smiles + return new_smi + + def assign_representative_molecule(self): + # create a molecule from fragment.vertices.copy + mapping = self.copy_and_map() + + # replace CuttingLabel with C14 structure to avoid fragment symmetry number issue + atoms = [] + additional_atoms = [] + additional_bonds = [] + for vertex in self.vertices: + mapped_vertex = mapping[vertex] + if isinstance(mapped_vertex, CuttingLabel): + # replace cutting label with atom C + atom_C1 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + for bondedAtom, bond in mapped_vertex.edges.items(): + new_bond = Bond(bondedAtom, atom_C1, order=bond.order) + + bondedAtom.edges[atom_C1] = new_bond + del bondedAtom.edges[mapped_vertex] + + atom_C1.edges[bondedAtom] = new_bond + + # add hydrogens and carbon to make it CC + atom_H1 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H2 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C2 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H3 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H4 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C3 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H5 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H6 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C4 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C5 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H7 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H8 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H9 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C6 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H10 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C7 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H11 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H12 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C8 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H13 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C9 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H14 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H15 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H16 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C10 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H17 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C11 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H18 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H19 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H20 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C12 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H21 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C13 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H22 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_C14 = Atom( + element=get_element("C"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H23 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H24 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atom_H25 = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + atoms.append(atom_C1) + + additional_atoms.extend( + [ + atom_H1, + atom_H2, + atom_H3, + atom_H4, + atom_H5, + atom_H6, + atom_H7, + atom_H8, + atom_H9, + atom_H10, + atom_H11, + atom_H12, + atom_H13, + atom_H14, + atom_H15, + atom_H16, + atom_H17, + atom_H18, + atom_H19, + atom_H20, + atom_H21, + atom_H22, + atom_H23, + atom_H24, + atom_H25, + atom_C2, + atom_C3, + atom_C4, + atom_C5, + atom_C6, + atom_C7, + atom_C8, + atom_C9, + atom_C10, + atom_C11, + atom_C12, + atom_C13, + atom_C14, + ] + ) + + additional_bonds.extend( + [ + Bond(atom_C1, atom_H1, 1), + Bond(atom_C1, atom_H2, 1), + Bond(atom_C2, atom_H3, 1), + Bond(atom_C2, atom_H4, 1), + Bond(atom_C1, atom_C2, 1), + Bond(atom_C2, atom_C3, 1), + Bond(atom_C3, atom_H5, 1), + Bond(atom_C3, atom_H6, 1), + Bond(atom_C3, atom_C4, 1), + Bond(atom_C4, atom_C5, 1), + Bond(atom_C5, atom_H7, 1), + Bond(atom_C5, atom_H8, 1), + Bond(atom_C5, atom_H9, 1), + Bond(atom_C4, atom_C6, 1), + Bond(atom_C6, atom_C7, 2), + Bond(atom_C6, atom_H10, 1), + Bond(atom_C7, atom_H11, 1), + Bond(atom_C7, atom_H12, 1), + Bond(atom_C4, atom_C8, 1), + Bond(atom_C8, atom_H13, 1), + Bond(atom_C8, atom_C9, 1), + Bond(atom_C9, atom_H14, 1), + Bond(atom_C9, atom_H15, 1), + Bond(atom_C9, atom_H16, 1), + Bond(atom_C8, atom_C10, 1), + Bond(atom_C10, atom_H17, 1), + Bond(atom_C10, atom_C11, 1), + Bond(atom_C11, atom_H18, 1), + Bond(atom_C11, atom_H19, 1), + Bond(atom_C11, atom_H20, 1), + Bond(atom_C10, atom_C12, 1), + Bond(atom_C12, atom_H21, 1), + Bond(atom_C12, atom_C13, 2), + Bond(atom_C13, atom_H22, 1), + Bond(atom_C13, atom_C14, 1), + Bond(atom_C14, atom_H23, 1), + Bond(atom_C14, atom_H24, 1), + Bond(atom_C14, atom_H25, 1), + ] + ) + + else: + atoms.append(mapped_vertex) + + mol_repr = Molecule() + mol_repr.atoms = atoms + for atom in additional_atoms: + mol_repr.add_atom(atom) + for bond in additional_bonds: + mol_repr.add_bond(bond) + # update connectivity + mol_repr.update() + + # create a species object from molecule + self.mol_repr = mol_repr + + return mapping + + def assign_representative_species(self): + from molecule.species import Species + + self.assign_representative_molecule() + self.species_repr = Species(molecule=[self.mol_repr]) + self.symmetry_number = self.get_symmetry_number() + self.species_repr.symmetry_number = self.symmetry_number + + def get_representative_molecule(self, mode="minimal", update=True): + if mode != "minimal": + raise RuntimeError( + 'Fragment.get_representative_molecule onyl supports mode="minimal"' + ) + # create a molecule from fragment.vertices.copy + mapping = self.copy_and_map() + + # replace CuttingLabel with H + atoms = [] + for vertex in self.vertices: + mapped_vertex = mapping[vertex] + if isinstance(mapped_vertex, CuttingLabel): + # replace cutting label with atom H + atom_H = Atom( + element=get_element("H"), + radical_electrons=0, + charge=0, + lone_pairs=0, + ) + + for bondedAtom, bond in mapped_vertex.edges.items(): + new_bond = Bond(bondedAtom, atom_H, order=bond.order) + + bondedAtom.edges[atom_H] = new_bond + del bondedAtom.edges[mapped_vertex] + + atom_H.edges[bondedAtom] = new_bond + + mapping[vertex] = atom_H + atoms.append(atom_H) + + else: + atoms.append(mapped_vertex) + + # Note: mapping is a dict with + # key: self.vertex and value: mol_repr.atom + mol_repr = Molecule() + mol_repr.atoms = atoms + if update: + mol_repr.update() + + return mol_repr, mapping diff --git a/molecule/molecule/fragment_utils.py b/molecule/molecule/fragment_utils.py index bc00f65..4a2b762 100644 --- a/molecule/molecule/fragment_utils.py +++ b/molecule/molecule/fragment_utils.py @@ -1,267 +1,312 @@ +from molecule.molecule.fragment import Fragment +from molecule.tools.canteramodel import Cantera +from molecule.chemkin import load_chemkin_file +import re +import os import numpy as np +import matplotlib.pyplot as plt + + +def match_sequences(seq1, seq2, rtol=1e-6): + ''' + Given two lists (each item is int or float): + seq1 and seq2 with same sum, the method returns + matched indices and values. + Example: + seq1 = [1, 3, 1] + seq2 = [2, 1, 2] + return: [[(0,0),1], + [(1,0),1], + [(1,1),1], + [(1,2),1], + [(2,2),1]] + ''' + sum_diff = sum(seq2) - sum(seq1) + assert ( + np.isclose(sum(seq1), sum(seq2), rtol=rtol) + ), "seq1 has different sum (diff={0}) than seq2.".format(sum_diff) + + # force the sum to be same if the difference + # is small enough + if sum_diff >= 0: + seq1[-1] = seq1[-1] + sum_diff + else: + seq2[-1] = seq2[-1] - sum_diff + + # make cumulative sequences + cum_seq1 = np.cumsum(seq1) + cum_seq2 = np.cumsum(seq2) + + # add index tags two both cumulative seqs + pin1 = 0 + pin2 = 0 + matched_indices = [] + matched_cum_values = [] + while pin1 < len(cum_seq1) and pin2 < len(cum_seq2): + matched_indices.append((pin1, pin2)) + + if cum_seq1[pin1] > cum_seq2[pin2]: + matched_cum_values.append(cum_seq2[pin2]) + pin2 += 1 + elif cum_seq1[pin1] < cum_seq2[pin2]: + matched_cum_values.append(cum_seq1[pin1]) + pin1 += 1 + else: + matched_cum_values.append(cum_seq2[pin2]) + pin1 += 1 + pin2 += 1 + + # get matches + matches = [] + for i in range(len(matched_indices)): + matched_index_tup = matched_indices[i] + matched_cum_value = matched_cum_values[i] + if i == 0: + previous_cum_value = 0 + else: + previous_cum_value = matched_cum_values[i - 1] + + matches.append( + [matched_index_tup, matched_cum_value - previous_cum_value]) + + return matches + + +def match_concentrations_with_same_sums(conc1, conc2, rtol=1e-6): + '''match_concentrations_with_same_sums + Given two lists with each item to be a tuple + (species label, concentration) + conc1 and conc2 with same total concentrations, + the method returns matched species labels and + concentrations. + Example: + conc1 = [('a', 1), + ('b', 3), + ('c', 1)] + conc2 = [('x', 2), + ('y', 1), + ('z', 2)] + return: [(('a','x'),1), + (('b','x'),1), + (('b','y'),1), + (('b','z'),1), + (('c','z'),1)] + ''' + labels1 = [tup[0] for tup in conc1] + labels2 = [tup[0] for tup in conc2] + + seq1 = [tup[1] for tup in conc1] + seq2 = [tup[1] for tup in conc2] + + matches_seq = FragList.match_sequences(seq1, seq2, rtol) + + matches_conc = [] + for match_seq in matches_seq: + matched_label_index1 = match_seq[0][0] + matched_label_index2 = match_seq[0][1] + matched_value = match_seq[1] + + matched_label1 = labels1[matched_label_index1] + matched_label2 = labels2[matched_label_index2] + match_conc = ((matched_label1, matched_label2), matched_value) + matches_conc.append(match_conc) + return matches_conc + + +def match_concentrations_with_different_sums(conc1, conc2): + """ + Given two lists with each item to be a tuple + (species label, concentration) + conc1 and conc2 with different total concentrations, + the method returns matched species labels and + concentrations. + Example: + conc1 = [('a', 1), + ('b', 3), + ('c', 1)] + conc2 = [('x', 2), + ('y', 1), + ('z', 10)] + return: [(('a','x', 'z', 'z'),1), + (('b','x', 'z', 'z'),1), + (('b','y', 'z', 'z'),1), + (('b','z', 'z'),1), + (('c','z', 'z'),1)] + """ + labels1 = [tup[0] for tup in conc1] + labels2 = [tup[0] for tup in conc2] + + seq1 = [tup[1] for tup in conc1] + seq2 = [tup[1] for tup in conc2] + + matches_conc = [] + pin1 = 0 + pin2 = 0 + val1 = seq1[pin1] + val2 = seq2[pin2] + + while True: + if val1 > val2: + match = ((labels1[pin1], labels2[pin2]), val2) + matches_conc.append(match) + val1 = val1 - val2 + pin2 += 1 + if pin2 == len(seq2): + break + val2 = seq2[pin2] + elif val1 < val2: + match = ((labels1[pin1], labels2[pin2]), val1) + matches_conc.append(match) + val2 = val2 - val1 + pin1 += 1 + if pin1 == len(seq1): + break + val1 = seq1[pin1] + else: + match = ((labels1[pin1], labels2[pin2]), val1) + matches_conc.append(match) + pin1 += 1 + pin2 += 1 + if pin1 == len(seq1): + break + val1 = seq1[pin1] + if pin2 == len(seq2): + break + val2 = seq2[pin2] + + # if pin2 first reaches the end + # append all the remaining seq1 to matches_conc + if pin2 == len(seq2) and pin1 < len(seq1): + remain_conc1 = [(labels1[pin1], val1)] + conc1[(pin1 + 1):] + matches_conc.extend(remain_conc1) + + # if pin1 first reaches the end + # let matches_conc match with remaining seq2 + elif pin1 == len(seq1) and pin2 < len(seq2): + remain_conc2 = [(labels2[pin2], val2)] + conc2[(pin2 + 1):] + matches_conc = FragList.match_concentrations_with_different_sums( + matches_conc, remain_conc2 + ) + + # if pin1 and pin2 reach the ends at same time + # matches_conc is ready to return + return matches_conc -def matches_resolve(matches, rr_ll_list): - """ - Sort out the pair of fragments and correct the amount. If the pair - contains additional cutting label, it will be added into new_r_l_moles - for further matching with other pairs. - """ - new_matches = [] - new_r_l_moles = [] - for match in matches: - pair = match[0] - value = match[1] - l_frag, r_frag = pair - - if l_frag not in rr_ll_list: - if r_frag not in rr_ll_list: - # cases like (L-Y, X-R) - new_matches.append((pair, value)) - else: - # cases like (L-Y, R-U1-R) - new_matches.append(((l_frag, r_frag, l_frag), value/2.0)) - else: - if r_frag not in rr_ll_list: - # cases like (L-W1-L, X-R) - new_matches.append(((r_frag, l_frag, r_frag), value/2.0)) - else: - # cases like (L-W1-L, R-U1-R) - new_r_l_moles.append((pair, value/2.0)) - - return new_matches, new_r_l_moles def shuffle(conc, seed=None): - """ - Randomly shuffle a list of fragments - """ - idx_arr = np.arange(len(conc)) - - if seed is not None: - np.random.seed(seed) - np.random.shuffle(idx_arr) - - return [conc[idx] for idx in idx_arr] - -def grind(conc, size): - """ - Split fragment concentrations into several repeating concentration units with specified size - """ - grinded_conc = [] - for label, c in conc: - times = int(c/size) - grinded_conc.extend([(label, size)]*times) - - if c-size*times > 0: - grinded_conc.append((label, c-size*times)) - - return grinded_conc - -def match_concentrations_with_same_sums(conc1, conc2, diff_tol=1e-6): - """ - Given two lists with each item to be a tuple - (species label, concentration) - conc1 and conc2 with same total concentrations, - the method returns matched species labels and - concentrations. - - Example: - - conc1 = [('a', 1), - ('b', 3), - ('c', 1)] - conc2 = [('x', 2), - ('y', 1), - ('z', 2)] - - return: [(('a','x'),1), - (('b','x'),1), - (('b','y'),1), - (('b','z'),1), - (('c','z'),1)] - """ - labels1 = [tup[0] for tup in conc1] - labels2 = [tup[0] for tup in conc2] - - seq1 = [tup[1] for tup in conc1] - seq2 = [tup[1] for tup in conc2] - - matches_seq = match_sequences(seq1, seq2, diff_tol) - - matches_conc = [] - for match_seq in matches_seq: - matched_label_index1 = match_seq[0][0] - matched_label_index2 = match_seq[0][1] - matched_value = match_seq[1] - - matched_label1 = labels1[matched_label_index1] - matched_label2 = labels2[matched_label_index2] - match_conc = ((matched_label1, matched_label2), matched_value) - matches_conc.append(match_conc) - - return matches_conc + """ + Randomly shuffle a list of fragments + """ + idx_arr = np.arange(len(conc)) + + if seed is not None: + np.random.seed(seed) + np.random.shuffle(idx_arr) + + return [conc[idx] for idx in idx_arr] -def match_concentrations_with_different_sums(conc1, conc2): - """ - Given two lists with each item to be a tuple - (species label, concentration) - conc1 and conc2 with different total concentrations, - the method returns matched species labels and - concentrations. - - Example: - - conc1 = [('a', 1), - ('b', 3), - ('c', 1)] - conc2 = [('x', 2), - ('y', 1), - ('z', 10)] - - return: [(('a','x', 'z', 'z'),1), - (('b','x', 'z', 'z'),1), - (('b','y', 'z', 'z'),1), - (('b','z', 'z'),1), - (('c','z', 'z'),1)] - """ - labels1 = [tup[0] for tup in conc1] - labels2 = [tup[0] for tup in conc2] - - seq1 = [tup[1] for tup in conc1] - seq2 = [tup[1] for tup in conc2] - - matches_conc = [] - pin1 = 0 - pin2 = 0 - val1 = seq1[pin1] - val2 = seq2[pin2] - - while True: - if val1 > val2: - match = ((labels1[pin1], labels2[pin2]), val2) - matches_conc.append(match) - val1 = val1 - val2 - pin2 += 1 - if pin2 == len(seq2): - break - val2 = seq2[pin2] - elif val1 < val2: - match = ((labels1[pin1], labels2[pin2]), val1) - matches_conc.append(match) - val2 = val2 - val1 - pin1 += 1 - if pin1 == len(seq1): - break - val1 = seq1[pin1] - else: - match = ((labels1[pin1], labels2[pin2]), val1) - matches_conc.append(match) - pin1 += 1 - pin2 += 1 - if pin1 == len(seq1): - break - val1 = seq1[pin1] - if pin2 == len(seq2): - break - val2 = seq2[pin2] - - # if pin2 first reaches the end - # append all the remaining seq1 to matches_conc - if pin2 == len(seq2) and pin1 < len(seq1): - remain_conc1 = [(labels1[pin1], val1)] + conc1[(pin1+1):] - matches_conc.extend(remain_conc1) - - # if pin1 first reaches the end - # let matches_conc match with remaining seq2 - elif pin1 == len(seq1) and pin2 < len(seq2): - remain_conc2 = [(labels2[pin2], val2)] + conc2[(pin2+1):] - matches_conc = match_concentrations_with_different_sums(matches_conc, remain_conc2) - - # if pin1 and pin2 reach the ends at same time - # matches_conc is ready to return - return matches_conc - -def match_sequences(seq1, seq2, diff_tol=1e-6): - """ - Given two lists (each item is int or float): - seq1 and seq2 with same sum, the method returns - matched indices and values. - - Example: - - seq1 = [1, 3, 1] - seq2 = [2, 1, 2] - - return: [[(0,0),1], - [(1,0),1], - [(1,1),1], - [(1,2),1], - [(2,2),1]] - """ - # check if sums are close to same - sum_diff = sum(seq2)-sum(seq1) - assert abs(sum_diff/1.0/sum(seq1)) <= diff_tol, 'seq1 has different sum (diff={0}) than seq2.'.format(sum_diff) - - # force the sum to be same if the difference - # is small enough - if sum_diff >=0: - seq1[-1] = seq1[-1] + sum_diff - else: - seq2[-1] = seq2[-1] - sum_diff - - # make cumulative sequences - cum_seq1 = [seq1[0]] - for item1 in seq1[1:]: - cum_seq1.append(cum_seq1[-1] + item1) - - cum_seq2 = [seq2[0]] - for item2 in seq2[1:]: - cum_seq2.append(cum_seq2[-1] + item2) - - # add index tags two both cumulative seqs - pin1 = 0 - pin2 = 0 - matched_indices = [] - matched_cum_values = [] - while pin1 < len(cum_seq1) and pin2 < len(cum_seq2): - - matched_indices.append((pin1, pin2)) - - if cum_seq1[pin1] > cum_seq2[pin2]: - matched_cum_values.append(cum_seq2[pin2]) - pin2 += 1 - elif cum_seq1[pin1] < cum_seq2[pin2]: - matched_cum_values.append(cum_seq1[pin1]) - pin1 += 1 - else: - matched_cum_values.append(cum_seq2[pin2]) - pin1 += 1 - pin2 += 1 - - # get matches - matches = [] - for i in range(len(matched_indices)): - matched_index_tup = matched_indices[i] - matched_cum_value = matched_cum_values[i] - if i == 0: - previous_cum_value = 0 - else: - previous_cum_value = matched_cum_values[i-1] - - matches.append([matched_index_tup, matched_cum_value - previous_cum_value]) - - return matches def flatten(combo): - """ - Given a combo nested `tuple`, e.g., - ((('LY', 'XR'), ('LWL', 'RUR')) - return a list of labels contained in - the combo ['LY', 'XR', 'LWL', 'RUR'] - """ - return_list = [] - for i in combo: - if isinstance(i, tuple): - return_list.extend(flatten(i)) - else: - return_list.append(i) - return return_list + """ + Given a combo nested `tuple`, e.g., + ((('LY', 'XR'), ('LWL', 'RUR')) + return a list of labels contained in + the combo ['LY', 'XR', 'LWL', 'RUR'] + """ + return_list = [] + for i in combo: + if isinstance(i, tuple): + return_list.extend(FragList.flatten(i)) + else: + return_list.append(i) + return return_list + + +# label should match the desired merging l/'abel on frag2 +def merge_frag_to_frag(frag1, frag2, label): + from molecule.molecule import Bond + from molecule.molecule.fragment import Fragment, CuttingLabel + + frag_spe1 = Fragment().from_smiles_like_string(frag1) + frag_spe2 = Fragment().from_smiles_like_string(frag2) + # find position of desired CuttingLabel + # need to find CuttingLabel on frag2 first + for vertex in frag_spe2.vertices: + if isinstance(vertex, CuttingLabel): + if vertex.symbol == label: + cut2 = vertex + + atom2 = list(cut2.edges.keys())[0] + frag_spe2.remove_atom(cut2) + break + + if cut2.symbol[0] == 'L': + Ctl = cut2.symbol.replace('L', 'R') + else: # that means this CuttingLabel is R something + + Ctl = cut2.symbol.replace('R', 'L') + + # merge to frag_spe1 + for vertex in frag_spe1.vertices: + if isinstance(vertex, CuttingLabel): + if vertex.symbol == Ctl: + cut1 = vertex + atom1 = list(cut1.edges.keys())[0] + frag_spe1.remove_atom(cut1) + break + + # new merged fragment + new_frag = frag_spe1.merge(frag_spe2) + new_frag.add_bond(Bond(atom1=atom1, atom2=atom2, order=1)) + new_frag = new_frag.copy(deep=True) + new_frag.update() + return new_frag # return Fragment obtl + + +def merge_frag_list(to_be_merged): + import os + # merges fragments in list from right to left + species_list = [] + ethylene = [] + newlist = [] + warnings = [] + + while len(to_be_merged) > 1: + + # second to last fragmentin list + frag1 = to_be_merged[-2].smiles + frag2 = to_be_merged[-1].smiles # last fragment in list + + if 'R' in frag1 and 'L' in frag2: + newfrag = FragList.merge_frag_to_frag(frag1, frag2, 'L') + + elif 'L' in frag1 and 'R' in frag2: + newfrag = FragList.merge_frag_to_frag(frag1, frag2, 'R') + + # warn user if last two fragments in list cannot be merged (no R/L + # combo to be made) + else: + print('Warning! Could not merge fragments {} and {}'.format( + frag1, frag2)) + + if 'L' in frag1 and 'L' in frag2: + newfrag = FragList.merge_frag_to_frag( + frag1.replace('L', 'R'), frag2, 'L') + if len(to_be_merged) > 2: + cut = len(to_be_merged) - 2 + newfraglist = to_be_merged[:cut] + + newfraglist.append(newfrag) + elif len(to_be_merged) == 2: + + newfraglist = [newfrag] + + to_be_merged = newfraglist + + to_be_merged = newfraglist + # newlist.append(newfraglist) # if done merging list, write final + # structure to list of smiles structures + + # print('{}% of fragments fully merged...'.format(np.round(100*(i+1)/len(flattened_matches_random)),1)) +# print(newfraglist) + return newfraglist diff --git a/molecule/molecule/group.pxd b/molecule/molecule/group.pxd index c6b00bd..985fb78 100644 --- a/molecule/molecule/group.pxd +++ b/molecule/molecule/group.pxd @@ -74,6 +74,10 @@ cdef class GroupAtom(Vertex): cpdef bint is_bonded_to_surface(self) except -2 + cpdef bint is_proton(self) + + cpdef bint is_electron(self) + cpdef bint is_oxygen(self) cpdef bint is_sulfur(self) @@ -190,6 +194,10 @@ cdef class Group(Graph): cpdef bint is_surface_site(self) except -2 + cpdef bint is_proton(self) + + cpdef bint is_electron(self) + cpdef bint contains_surface_site(self) except -2 cpdef list get_surface_sites(self) diff --git a/molecule/molecule/group.py b/molecule/molecule/group.py index 5dab8e6..67fbdf4 100644 --- a/molecule/molecule/group.py +++ b/molecule/molecule/group.py @@ -44,6 +44,7 @@ from molecule.molecule.atomtype import ATOMTYPES, allElements, nonSpecifics, get_features, AtomType from molecule.molecule.element import PeriodicSystem from molecule.molecule.graph import Vertex, Edge, Graph +from molecule.molecule.fragment import CuttingLabel ################################################################################ @@ -80,7 +81,7 @@ class GroupAtom(Vertex): order to match. """ - def __init__(self, atomtype=None, radical_electrons=None, charge=None, label='', lone_pairs=None, site=None, morphology=None, + def __init__(self, atomtype=None, radical_electrons=None, charge=None, label='', lone_pairs=None, site=None, morphology=None, props=None): Vertex.__init__(self) self.atomtype = atomtype or [] @@ -115,7 +116,7 @@ def __reduce__(self): atomtype = self.atomtype if atomtype is not None: atomtype = [a.label for a in atomtype] - return (GroupAtom, (atomtype, self.radical_electrons, self.charge, self.label, self.lone_pairs, self.site, + return (GroupAtom, (atomtype, self.radical_electrons, self.charge, self.label, self.lone_pairs, self.site, self.morphology, self.props), d) def __setstate__(self, d): @@ -271,6 +272,54 @@ def _lose_radical(self, radical): # Set the new radical electron counts self.radical_electrons = radical_electrons + def _gain_charge(self, charge): + """ + Update the atom group as a result of applying a GAIN_CHARGE action, + where `charge` specifies the charge gained. + """ + atomtype = [] + + for atom in self.atomtype: + atomtype.extend(atom.increment_charge) + + if any([len(atom.increment_charge) == 0 for atom in self.atomtype]): + raise ActionError('Unable to update GroupAtom due to GAIN_CHARGE action: ' + 'Unknown atom type produced from set "{0}".'.format(self.atomtype)) + + if isinstance(self.charge,list): + charges = [] + for c in self.charge: + charges.append(c+charge) + self.charge = charges + else: + self.charge += 1 + + self.atomtype = list(set(atomtype)) + + def _lose_charge(self, charge): + """ + Update the atom group as a result of applying a LOSE_CHARGE action, + where `charge` specifies lost charge. + """ + atomtype = [] + + for atom in self.atomtype: + atomtype.extend(atom.decrement_charge) + + if any([len(atomtype.decrement_charge) == 0 for atomtype in self.atomtype]): + raise ActionError('Unable to update GroupAtom due to LOSE_CHARGE action: ' + 'Unknown atom type produced from set "{0}".'.format(self.atomtype)) + + if isinstance(self.charge,list): + charges = [] + for c in self.charge: + charges.append(c-charge) + self.charge = charges + else: + self.charge -= 1 + + self.atomtype = list(set(atomtype)) + def _gain_pair(self, pair): """ Update the atom group as a result of applying a GAIN_PAIR action, @@ -342,8 +391,12 @@ def apply_action(self, action): self._break_bond(action[2]) elif act == 'GAIN_RADICAL': self._gain_radical(action[2]) + elif act == 'GAIN_CHARGE': + self._gain_charge(action[2]) elif act == 'LOSE_RADICAL': self._lose_radical(action[2]) + elif act == 'LOSE_CHARGE': + self._lose_charge(action[2]) elif action[0].upper() == 'GAIN_PAIR': self._gain_pair(action[2]) elif action[0].upper() == 'LOSE_PAIR': @@ -357,7 +410,7 @@ def equivalent(self, other, strict=True): where `other` can be either an :class:`Atom` or an :class:`GroupAtom` object. When comparing two :class:`GroupAtom` objects, this function respects wildcards, e.g. ``R!H`` is equivalent to ``C``. - + """ cython.declare(group=GroupAtom) if not strict: @@ -453,7 +506,7 @@ def is_specific_case_of(self, other): """ Returns ``True`` if `self` is the same as `other` or is a more specific case of `other`. Returns ``False`` if some of `self` is not - included in `other` or they are mutually exclusive. + included in `other` or they are mutually exclusive. """ cython.declare(group=GroupAtom) if not isinstance(other, GroupAtom): @@ -550,6 +603,18 @@ def is_bonded_to_surface(self): return True return False + def is_electron(self): + """ + Return ``True`` if the atom represents a surface site or ``False`` if not. + """ + return self.atomtype[0] == ATOMTYPES['e'] + + def is_proton(self): + """ + Return ``True`` if the atom represents a surface site or ``False`` if not. + """ + return self.atomtype[0] == ATOMTYPES['H+'] + def is_oxygen(self): """ Return ``True`` if the atom represents an oxygen atom or ``False`` if not. @@ -694,6 +759,7 @@ def make_sample_atom(self): 'I': 3, 'Ar': 4, 'X': 0, + 'e': 0 } for element_label in allElements: @@ -871,7 +937,7 @@ def is_single(self, wildcards=False): not. If `wildcards` is ``False`` we return False anytime there is more than one bond order, otherwise we return ``True`` if any of the options are single. - + NOTE: we can replace the absolute value relation with math.isclose when we swtich to python 3.5+ """ @@ -947,7 +1013,8 @@ def is_van_der_waals(self, wildcards=False): return False else: return abs(self.order[0]) <= 1e-9 and len(self.order) == 1 - + + def is_reaction_bond(self, wildcards=False): """ Return ``True`` if the bond represents a van der Waals bond or ``False`` if @@ -957,7 +1024,7 @@ def is_reaction_bond(self, wildcards=False): """ if wildcards: for order in self.order: - if abs(order[0]-0.05) <= 1e-9: + if abs(order[0] - 0.05) <= 1e-9: return True else: return False @@ -995,7 +1062,7 @@ def is_hydrogen_bond(self, wildcards=False): return False else: return abs(self.order[0] - 0.1) <= 1e-9 and len(self.order) == 1 - + def is_reaction_bond(self, wildcards=False): """ Return ``True`` if the bond represents a reaction bond or ``False`` if @@ -1121,13 +1188,13 @@ class Group(Graph): """ A representation of a molecular substructure group using a graph data type, extending the :class:`Graph` class. The attributes are: - + =================== =================== ==================================== Attribute Type Description =================== =================== ==================================== `atoms` ``list`` Aliases for the `vertices` storing :class:`GroupAtom` `multiplicity` ``list`` Range of multiplicities accepted for the group - `props` ``dict`` Dictionary of arbitrary properties/flags classifying state of Group object + `props` ``dict`` Dictionary of arbitrary properties/flags classifying state of Group object `metal` ``list`` List of metals accepted for the group `facet` ``list`` List of facets accepted for the group =================== =================== ==================================== @@ -1262,6 +1329,14 @@ def get_surface_sites(self): cython.declare(atom=GroupAtom) return [atom for atom in self.atoms if atom.is_surface_site()] + def is_proton(self): + """Returns ``True`` iff the group is a proton""" + return len(self.atoms) == 1 and self.atoms[0].is_proton() + + def is_electron(self): + """Returns ``True`` iff the group is an electron""" + return len(self.atoms) == 1 and self.atoms[0].is_electron() + def remove_atom(self, atom): """ Remove `atom` and all bonds associated with it from the graph. Does @@ -1344,6 +1419,8 @@ def update_charge(self): and radical electrons. This method is used for products of specific families with recipes that modify charges. """ for atom in self.atoms: + if isinstance(atom, CuttingLabel): + continue if (len(atom.charge) == 1) and (len(atom.lone_pairs) == 1) and (len(atom.radical_electrons) == 1): # if the charge of the group is not labeled, then no charge update will be # performed. If there multiple charges are assigned, no update either. @@ -1404,7 +1481,7 @@ def clear_reg_dims(self): for bd in self.get_all_edges(): bd.reg_dim = [[], []] - def get_extensions(self, r=None, r_bonds=[1,2,3,1.5,4], r_un=[0,1,2,3], basename='', atm_ind=None, atm_ind2=None, n_splits=None): + def get_extensions(self, r=None, r_bonds=None, r_un=None, basename='', atm_ind=None, atm_ind2=None, n_splits=None): """ generate all allowed group extensions and their complements note all atomtypes except for elements and r/r!H's must be removed @@ -1413,7 +1490,10 @@ def get_extensions(self, r=None, r_bonds=[1,2,3,1.5,4], r_un=[0,1,2,3], basename extents=list, RnH=list, typ=list) extents = [] - + if r_bonds is None: + r_bonds = [1, 1.5, 2, 3, 4] + if r_un is None: + r_un = [0, 1, 2, 3] if n_splits is None: n_splits = len(self.split()) @@ -1441,6 +1521,7 @@ def get_extensions(self, r=None, r_bonds=[1,2,3,1.5,4], r_un=[0,1,2,3], basename extents.extend(self.specify_atom_extensions(i, basename, RnH)) elif typ[0].label == 'Rx': extents.extend(self.specify_atom_extensions(i, basename, r)) + else: extents.extend(self.specify_atom_extensions(i, basename, typ)) else: @@ -1454,7 +1535,6 @@ def get_extensions(self, r=None, r_bonds=[1,2,3,1.5,4], r_un=[0,1,2,3], basename elif typ[0].label == 'Rx': extents.extend( self.specify_atom_extensions(i, basename, list(set(atm.reg_dim_atm[0]) & set(r)))) - else: extents.extend( self.specify_atom_extensions(i, basename, list(set(typ) & set(atm.reg_dim_atm[0])))) @@ -1569,10 +1649,10 @@ def specify_atom_extensions(self, i, basename, r): old_atom_type = grp.atoms[i].atomtype grp.atoms[i].atomtype = [item] grpc.atoms[i].atomtype = list(Rset - {item}) - + if len(grpc.atoms[i].atomtype) == 0: grpc = None - + if len(old_atom_type) > 1: labelList = [] old_atom_type_str = '' @@ -1636,10 +1716,10 @@ def specify_unpaired_extensions(self, i, basename, r_un): grpc = deepcopy(self) grp.atoms[i].radical_electrons = [item] grpc.atoms[i].radical_electrons = list(Rset - {item}) - + if len(grpc.atoms[i].radical_electrons) == 0: grpc = None - + atom_type = grp.atoms[i].atomtype if len(atom_type) > 1: @@ -1738,7 +1818,7 @@ def specify_bond_extensions(self, i, j, basename, r_bonds): label_list = [] Rbset = set(r_bonds) bdict = {1: '-', 2: '=', 3: '#', 1.5: '-=', 4: '$', 0.05: '..', 0: '--'} - bstrdict = {'S':1, 'D': 2, 'T':3, 'B':1.5, 'Q':4, 'R':0.05, 'vdW': 0} + for bd in r_bonds: grp = deepcopy(self) grpc = deepcopy(self) @@ -1746,10 +1826,10 @@ def specify_bond_extensions(self, i, j, basename, r_bonds): grp.atoms[j].bonds[grp.atoms[i]].order = [bd] grpc.atoms[i].bonds[grpc.atoms[j]].order = list(Rbset - {bd}) grpc.atoms[j].bonds[grpc.atoms[i]].order = list(Rbset - {bd}) - + if len(list(Rbset - {bd})) == 0: grpc = None - + atom_type_i = grp.atoms[i].atomtype atom_type_j = grp.atoms[j].atomtype @@ -1774,12 +1854,14 @@ def specify_bond_extensions(self, i, j, basename, r_bonds): else: atom_type_j_str = atom_type_j[0].label - b = None + b = None for v in bdict.keys(): if abs(v - bd) < 1e-4: b = bdict[v] + + grps.append((grp, grpc, - basename + '_Sp-' + str(i + 1) + atom_type_i_str + b + str(j + 1) + atom_type_j_str, + basename + '_Sp-' + str(i + 1) + atom_type_i_str + b + str(j + 1) + atom_type_j_str, 'bondExt', (i, j))) return grps @@ -2069,7 +2151,7 @@ def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False): else: if group.facet: return [] - + # Do the isomorphism comparison return Graph.find_subgraph_isomorphisms(self, other, initial_map, save_order=save_order) @@ -2085,7 +2167,7 @@ def is_identical(self, other, save_order=False): if not isinstance(other, Group): raise TypeError( 'Got a {0} object for parameter "other", when a Group object is required.'.format(other.__class__)) - # An identical group is always a child of itself and + # An identical group is always a child of itself and # is the only case where that is true. Therefore # if we do both directions of isSubgraphIsmorphic, we need # to get True twice for it to be identical @@ -2901,10 +2983,24 @@ def make_sample_molecule(self): group_atom = mol_to_group[atom] else: raise UnexpectedChargeError(graph=new_molecule) - if atom.charge in group_atom.atomtype[0].charge: - # declared charge in atomtype is same as new charge + # check hardcoded atomtypes + positive_charged = ['H+', + 'Csc', 'Cdc', + 'N3sc', 'N5sc', 'N5dc', 'N5ddc', 'N5tc', 'N5b', + 'O2sc', 'O4sc', 'O4dc', 'O4tc', + 'P5sc', 'P5dc', 'P5ddc', 'P5tc', 'P5b', + 'S2sc', 'S4sc', 'S4dc', 'S4tdc', 'S6sc', 'S6dc', 'S6tdc'] + negative_charged = ['e', + 'C2sc', 'C2dc', 'C2tc', + 'N0sc', 'N1sc', 'N1dc', 'N5dddc', + 'O0sc', + 'P0sc', 'P1sc', 'P1dc', 'P5sc', + 'S0sc', 'S2sc', 'S2dc', 'S2tc', 'S4sc', 'S4dc', 'S4tdc', 'S6sc', 'S6dc', 'S6tdc'] + if atom.charge > 0 and any([group_atom.atomtype[0] is ATOMTYPES[x] or ATOMTYPES[x].is_specific_case_of(group_atom.atomtype[0]) for x in positive_charged]): + pass + elif atom.charge < 0 and any([group_atom.atomtype[0] is ATOMTYPES[x] or ATOMTYPES[x].is_specific_case_of(group_atom.atomtype[0]) for x in negative_charged]): pass - elif atom.charge in group_atom.charge: + elif atom.charge in group_atom.atomtype[0].charge: # declared charge in original group is same as new charge pass else: diff --git a/molecule/molecule/inchi.pxd b/molecule/molecule/inchi.pxd index 6be0d08..a60dc71 100644 --- a/molecule/molecule/inchi.pxd +++ b/molecule/molecule/inchi.pxd @@ -45,8 +45,6 @@ cpdef bint _has_unexpected_lone_pairs(Molecule mol) cpdef list _get_unpaired_electrons(Molecule mol) -cpdef Molecule _generate_minimum_resonance_isomer(Molecule mol) - cpdef list _compute_agglomerate_distance(list agglomerates, Molecule mol) cpdef bint _is_valid_combo(list combo, Molecule mol, list distances) diff --git a/molecule/molecule/inchi.py b/molecule/molecule/inchi.py index 5d69f94..b0803ec 100644 --- a/molecule/molecule/inchi.py +++ b/molecule/molecule/inchi.py @@ -367,44 +367,6 @@ def _get_unpaired_electrons(mol): return sorted(locations) -def _generate_minimum_resonance_isomer(mol): - """ - Select the resonance isomer that is isomorphic to the parameter isomer, with the lowest unpaired - electrons descriptor. - - First, we generate all isomorphic resonance isomers. - Next, we return the candidate with the lowest unpaired electrons metric. - - The metric is a sorted list with indices of the atoms that bear an unpaired electron - - This function is currently deprecated since InChI effectively eliminates resonance, - see InChI, the IUPAC International Chemical Identifier, J. Cheminform 2015, 7, 23, doi: 10.1186/s13321-015-0068-4 - """ - - cython.declare( - candidates=list, - sel=Molecule, - cand=Molecule, - metric_sel=list, - metric_cand=list, - ) - - warnings.warn("The _generate_minimum_resonance_isomer method is no longer used" - " and may be removed in RMG version 2.3.", DeprecationWarning) - - candidates = resonance.generate_isomorphic_resonance_structures(mol, saturate_h=True) - - sel = candidates[0] - 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 - - return sel - - def _compute_agglomerate_distance(agglomerates, mol): """ Iterates over a list of lists containing atom indices. @@ -640,13 +602,13 @@ def create_augmented_layers(mol): else: molcopy = mol.copy(deep=True) + rdkitmol = to_rdkit_mol(molcopy, remove_h=True) + _, auxinfo = Chem.MolToInchiAndAuxInfo(rdkitmol, options='-SNon') # suppress stereo warnings + hydrogens = [at for at in molcopy.atoms if at.number == 1] for h in hydrogens: molcopy.remove_atom(h) - rdkitmol = to_rdkit_mol(molcopy) - _, auxinfo = Chem.MolToInchiAndAuxInfo(rdkitmol, options='-SNon') # suppress stereo warnings - # extract the atom numbers from N-layer of auxiliary info: atom_indices = _parse_n_layer(auxinfo) atom_indices = [atom_indices.index(i + 1) for i, atom in enumerate(molcopy.atoms)] diff --git a/molecule/molecule/molecule.pxd b/molecule/molecule/molecule.pxd index 15d9b7f..f943c65 100644 --- a/molecule/molecule/molecule.pxd +++ b/molecule/molecule/molecule.pxd @@ -56,6 +56,10 @@ cdef class Atom(Vertex): cpdef Vertex copy(self) + cpdef bint is_electron(self) + + cpdef bint is_proton(self) + cpdef bint is_hydrogen(self) cpdef bint is_non_hydrogen(self) @@ -89,7 +93,11 @@ cdef class Atom(Vertex): cpdef increment_radical(self) cpdef decrement_radical(self) - + + cpdef increment_charge(self) + + cpdef decrement_charge(self) + cpdef set_lone_pairs(self, int lone_pairs) cpdef increment_lone_pairs(self) @@ -166,6 +174,10 @@ cdef class Molecule(Graph): cpdef bint has_bond(self, Atom atom1, Atom atom2) + cpdef bint is_electron(self) + + cpdef bint is_proton(self) + cpdef bint contains_surface_site(self) cpdef bint is_surface_site(self) @@ -224,14 +236,14 @@ cdef class Molecule(Graph): bint raise_charge_exception=?, bint check_consistency=?) cpdef from_xyz(self, np.ndarray atomic_nums, np.ndarray coordinates, float critical_distance_factor=?, bint raise_atomtype_exception=?) - - cpdef str to_inchi(self) - cpdef str to_augmented_inchi(self) + cpdef str to_inchi(self, str backend=?) - cpdef str to_inchi_key(self) + cpdef str to_augmented_inchi(self, str backend=?) - cpdef str to_augmented_inchi_key(self) + cpdef str to_inchi_key(self, str backend=?) + + cpdef str to_augmented_inchi_key(self, str backend=?) cpdef str to_smiles(self) @@ -289,6 +301,8 @@ cdef class Molecule(Graph): cpdef list get_adatoms(self) + cpdef bint is_multidentate(self) + cpdef list get_desorbed_molecules(self) cdef atom_id_counter diff --git a/molecule/molecule/molecule.py b/molecule/molecule/molecule.py index c353a5a..c3d588a 100644 --- a/molecule/molecule/molecule.py +++ b/molecule/molecule/molecule.py @@ -58,6 +58,7 @@ from molecule.molecule.graph import Vertex, Edge, Graph, get_vertex_connectivity_value from molecule.molecule.kekulize import kekulize from molecule.molecule.pathfinder import find_shortest_path +from molecule.molecule.fragment import CuttingLabel ################################################################################ @@ -178,7 +179,7 @@ def __eq__(self, other): def __lt__(self, other): """Define less than comparison. For comparing against other Atom objects (e.g. when sorting).""" - if isinstance(other, Atom): + if issubclass(type(other), Vertex): return self.sorting_key < other.sorting_key else: raise NotImplementedError('Cannot perform less than comparison between Atom and ' @@ -186,7 +187,7 @@ def __lt__(self, other): def __gt__(self, other): """Define greater than comparison. For comparing against other Atom objects (e.g. when sorting).""" - if isinstance(other, Atom): + if issubclass(type(other), Vertex): return self.sorting_key > other.sorting_key else: raise NotImplementedError('Cannot perform greater than comparison between Atom and ' @@ -233,6 +234,7 @@ def equivalent(self, other, strict=True): and self.atomtype is atom.atomtype and self.site == atom.site and self.morphology == atom.morphology) + else: return self.element is atom.element elif isinstance(other, gr.GroupAtom): @@ -353,6 +355,23 @@ def copy(self): a.props = deepcopy(self.props) return a + def is_electron(self): + """ + Return ``True`` if the atom represents an electron or ``False`` if + not. + """ + return self.element.number == -1 + + def is_proton(self): + """ + Return ``True`` if the atom represents a proton or ``False`` if + not. + """ + + if self.element.number == 1 and self.charge == 1: + return True + return False + def is_hydrogen(self): """ Return ``True`` if the atom represents a hydrogen atom or ``False`` if @@ -509,6 +528,18 @@ def decrement_radical(self): raise gr.ActionError('Unable to update Atom due to LOSE_RADICAL action: ' 'Invalid radical electron set "{0}".'.format(self.radical_electrons)) + def increment_charge(self): + """ + Update the atom pattern as a result of applying a GAIN_CHARGE action + """ + self.charge += 1 + + def decrement_charge(self): + """ + Update the atom pattern as a result of applying a LOSE_CHARGE action + """ + self.charge -= 1 + def set_lone_pairs(self, lone_pairs): """ Set the number of lone electron pairs. @@ -550,6 +581,10 @@ def update_charge(self): if self.is_surface_site(): self.charge = 0 return + if self.is_electron(): + self.charge = -1 + return + valence_electron = elements.PeriodicSystem.valence_electrons[self.symbol] order = self.get_total_bond_order() self.charge = valence_electron - order - self.radical_electrons - 2 * self.lone_pairs @@ -572,6 +607,10 @@ def apply_action(self, action): for i in range(action[2]): self.increment_radical() elif act == 'LOSE_RADICAL': for i in range(abs(action[2])): self.decrement_radical() + elif act == 'GAIN_CHARGE': + for i in range(action[2]): self.increment_charge() + elif act == 'LOSE_CHARGE': + for i in range(abs(action[2])): self.decrement_charge() elif action[0].upper() == 'GAIN_PAIR': for i in range(action[2]): self.increment_lone_pairs() elif action[0].upper() == 'LOSE_PAIR': @@ -844,6 +883,13 @@ def is_quadruple(self): """ return self.is_order(4) + def is_double_or_triple(self): + """ + Return ``True`` if the bond represents a double or triple bond or ``False`` + if not. + """ + return self.is_order(2) or self.is_order(3) + def is_benzene(self): """ Return ``True`` if the bond represents a benzene bond or ``False`` if @@ -1164,10 +1210,31 @@ def contains_surface_site(self): return True return False + def number_of_surface_sites(self): + """ + Returns the number of surface sites in the molecule. + e.g. 2 for a bidentate adsorbate + """ + cython.declare(atom=Atom) + cython.declare(count=cython.int) + count = 0 + for atom in self.atoms: + if atom.is_surface_site(): + count += 1 + return count + def is_surface_site(self): """Returns ``True`` iff the molecule is nothing but a surface site 'X'.""" return len(self.atoms) == 1 and self.atoms[0].is_surface_site() + def is_electron(self): + """Returns ``True`` iff the molecule is nothing but an electron 'e'.""" + return len(self.atoms) == 1 and self.atoms[0].is_electron() + + def is_proton(self): + """Returns ``True`` iff the molecule is nothing but a proton 'H+'.""" + return len(self.atoms) == 1 and self.atoms[0].is_proton() + def remove_atom(self, atom): """ Remove `atom` and all bonds associated with it from the graph. Does @@ -1214,17 +1281,22 @@ def sort_atoms(self): for index, vertex in enumerate(self.vertices): vertex.sorting_label = index + def update_charge(self): + + for atom in self.atoms: + if not isinstance(atom, CuttingLabel): + atom.update_charge() + def update(self, log_species=True, raise_atomtype_exception=True, sort_atoms=True): """ - Update the charge and atom types of atoms. + Update the lone_pairs, charge, and atom types of atoms. Update multiplicity, and sort atoms (if ``sort_atoms`` is ``True``) Does not necessarily update the connectivity values (which are used in isomorphism checks) If you need that, call update_connectivity_values() """ - for atom in self.atoms: - atom.update_charge() - + self.update_lone_pairs() + self.update_charge() self.update_atomtypes(log_species=log_species, raise_exception=raise_atomtype_exception) self.update_multiplicity() if sort_atoms: @@ -1764,9 +1836,13 @@ def _repr_png_(self): os.unlink(temp_file_name) return png - def from_inchi(self, inchistr, backend='try-all', raise_atomtype_exception=True): + def from_inchi(self, inchistr, backend='openbabel-first', raise_atomtype_exception=True): """ Convert an InChI string `inchistr` to a molecular structure. + + RDKit and Open Babel are the two backends used in RMG. It is possible to use a + single backend or try different backends in sequence. The available options for the ``backend`` + argument: 'openbabel-first'(default), 'rdkit-first', 'rdkit', or 'openbabel'. """ translator.from_inchi(self, inchistr, backend, raise_atomtype_exception=raise_atomtype_exception) return self @@ -1778,9 +1854,13 @@ def from_augmented_inchi(self, aug_inchi, raise_atomtype_exception=True): translator.from_augmented_inchi(self, aug_inchi, raise_atomtype_exception=raise_atomtype_exception) return self - def from_smiles(self, smilesstr, backend='try-all', raise_atomtype_exception=True): + def from_smiles(self, smilesstr, backend='openbabel-first', raise_atomtype_exception=True): """ Convert a SMILES string `smilesstr` to a molecular structure. + + RDKit and Open Babel are the two backends used in RMG. It is possible to use a + single backend or try different backends in sequence. The available options for the ``backend`` + argument: 'openbabel-first'(default), 'rdkit-first', 'rdkit', or 'openbabel'. """ translator.from_smiles(self, smilesstr, backend, raise_atomtype_exception=raise_atomtype_exception) return self @@ -1795,7 +1875,7 @@ def from_smarts(self, smartsstr, raise_atomtype_exception=True): return self def from_adjacency_list(self, adjlist, saturate_h=False, raise_atomtype_exception=True, - raise_charge_exception=True, check_consistency=True): + raise_charge_exception=False, check_consistency=True): """ Convert a string adjacency list `adjlist` to a molecular structure. Skips the first line (assuming it's a label) unless `withLabel` is @@ -1859,62 +1939,78 @@ def to_single_bonds(self, raise_atomtype_exception=True): new_mol.update_atomtypes(raise_exception=raise_atomtype_exception) return new_mol - def to_inchi(self): + def to_inchi(self, backend='rdkit-first'): """ Convert a molecular structure to an InChI string. Uses `RDKit `_ to perform the conversion. Perceives aromaticity. - + or - + Convert a molecular structure to an InChI string. Uses `OpenBabel `_ to perform the conversion. + + It is possible to use a single backend or try different backends in sequence. + The available options for the ``backend`` argument: 'rdkit-first'(default), + 'openbabel-first', 'rdkit', or 'openbabel'. """ try: - return translator.to_inchi(self) + return translator.to_inchi(self, backend=backend) except: logging.exception(f"Error for molecule \n{self.to_adjacency_list()}") raise - def to_augmented_inchi(self): + def to_augmented_inchi(self, backend='rdkit-first'): """ Adds an extra layer to the InChI denoting the multiplicity of the molecule. - + Separate layer with a forward slash character. + + RDKit and Open Babel are the two backends used in RMG. It is possible to use a + single backend or try different backends in sequence. The available options for the ``backend`` + argument: 'rdkit-first'(default), 'openbabel-first', 'rdkit', or 'openbabel'. """ try: - return translator.to_inchi(self, aug_level=2) + return translator.to_inchi(self, backend=backend, aug_level=2) except: logging.exception(f"Error for molecule \n{self.to_adjacency_list()}") raise - def to_inchi_key(self): + def to_inchi_key(self, backend='rdkit-first'): """ Convert a molecular structure to an InChI Key string. Uses `OpenBabel `_ to perform the conversion. - - or - + + or + Convert a molecular structure to an InChI Key string. Uses `RDKit `_ to perform the conversion. + + It is possible to use a single backend or try different backends in sequence. + The available options for the ``backend`` argument: 'rdkit-first'(default), + 'openbabel-first', 'rdkit', or 'openbabel'. """ try: - return translator.to_inchi_key(self) + return translator.to_inchi_key(self, backend=backend) except: logging.exception(f"Error for molecule \n{self.to_adjacency_list()}") raise - def to_augmented_inchi_key(self): + def to_augmented_inchi_key(self, backend='rdkit-first'): """ Adds an extra layer to the InChIKey denoting the multiplicity of the molecule. Simply append the multiplicity string, do not separate by a character like forward slash. + + RDKit and Open Babel are the two backends used in RMG. It is possible to use a + single backend or try different backends in sequence. The available options for the ``backend`` + argument: 'rdkit-first'(default), 'openbabel-first', 'rdkit', or 'openbabel'. """ try: - return translator.to_inchi_key(self, aug_level=2) + return translator.to_inchi_key(self, backend=backend, aug_level=2) except: logging.exception(f"Error for molecule \n{self.to_adjacency_list()}") raise @@ -1976,7 +2072,7 @@ def find_h_bonds(self): ONinds = [n for n, a in enumerate(self.atoms) if a.is_oxygen() or a.is_nitrogen()] for i, atm1 in enumerate(self.atoms): - if atm1.atomtype.label == 'H': + if atm1.atomtype.label == 'H0': atm_covs = [q for q in atm1.bonds.keys()] if len(atm_covs) > 1: # H is already H bonded continue @@ -2239,10 +2335,15 @@ def is_aryl_radical(self, aromatic_rings=None, save_order=False): def generate_resonance_structures(self, keep_isomorphic=False, filter_structures=True, save_order=False): """Returns a list of resonance structures of the molecule.""" - return resonance.generate_resonance_structures(self, keep_isomorphic=keep_isomorphic, + + try: + return resonance.generate_resonance_structures(self, keep_isomorphic=keep_isomorphic, filter_structures=filter_structures, save_order=save_order, ) + except: + logging.warning("Resonance structure generation failed for {}".format(self)) + return [self.copy(deep=True)] def get_url(self): """ @@ -2272,7 +2373,7 @@ def update_lone_pairs(self): """ cython.declare(atom1=Atom, atom2=Atom, bond12=Bond, order=float) for atom1 in self.vertices: - if atom1.is_hydrogen() or atom1.is_surface_site() or atom1.is_lithium(): + if atom1.is_hydrogen() or atom1.is_surface_site() or atom1.is_electron() or atom1.is_lithium(): atom1.lone_pairs = 0 else: order = atom1.get_total_bond_order() @@ -2448,7 +2549,35 @@ def get_aromatic_rings(self, rings=None, save_order=False): if rings is None: rings = self.get_relevant_cycles() - rings = [ring for ring in rings if len(ring) == 6] + + def filter_fused_rings(_rings): + """ + Given a list of rings, remove ones which share more than 2 atoms. + """ + cython.declare(toRemove=set, i=cython.int, j=cython.int, toRemoveSorted=list) + + if len(_rings) < 2: + return _rings + + to_remove = set() + for i, j in itertools.combinations(range(len(_rings)), 2): + if len(set(_rings[i]) & set(_rings[j])) > 2: + to_remove.add(i) + to_remove.add(j) + + to_remove_sorted = sorted(to_remove, reverse=True) + + for i in to_remove_sorted: + del _rings[i] + + return _rings + + # Remove rings that share more than 3 atoms, since they cannot be planar + rings = filter_fused_rings(rings) + + # Only keep rings with exactly 6 atoms, since RMG can only handle aromatic benzene + rings = [ring for ring in rings if len(ring) == 6] + if not rings: return [], [] @@ -2742,6 +2871,16 @@ def get_surface_sites(self): cython.declare(atom=Atom) return [atom for atom in self.atoms if atom.is_surface_site()] + def is_multidentate(self): + """ + Return ``True`` if the adsorbate contains at least two binding sites, + or ``False`` otherwise. + """ + cython.declare(atom=Atom) + if len([atom for atom in self.atoms if atom.is_surface_site()])>=2: + return True + return False + def get_adatoms(self): """ Get a list of adatoms in the molecule. diff --git a/molecule/molecule/pathfinder.pxd b/molecule/molecule/pathfinder.pxd index 2823882..5091e44 100644 --- a/molecule/molecule/pathfinder.pxd +++ b/molecule/molecule/pathfinder.pxd @@ -58,3 +58,7 @@ cpdef list find_N5dc_radical_delocalization_paths(Vertex atom1) cpdef bint is_atom_able_to_gain_lone_pair(Vertex atom) cpdef bint is_atom_able_to_lose_lone_pair(Vertex atom) + +cpdef list find_adsorbate_delocalization_paths(Vertex atom1) + +cpdef list find_adsorbate_conjugate_delocalization_paths(Vertex atom1) \ No newline at end of file diff --git a/molecule/molecule/pathfinder.py b/molecule/molecule/pathfinder.py index 03bd1da..72c19c4 100644 --- a/molecule/molecule/pathfinder.py +++ b/molecule/molecule/pathfinder.py @@ -480,3 +480,58 @@ def is_atom_able_to_lose_lone_pair(atom): return (((atom.is_nitrogen() or atom.is_sulfur()) and atom.lone_pairs in [1, 2, 3]) or (atom.is_oxygen() and atom.lone_pairs in [2, 3]) or atom.is_carbon() and atom.lone_pairs == 1) + + +def find_adsorbate_delocalization_paths(atom1): + """ + Find all multidentate adsorbates which have a bonding configuration X-C-C-X. + Examples: + + - XCXC, XCHXCH, XCXCH, where X is the surface site. The adsorption site X + is always placed on the left-hand side of the adatom and every adatom + is bonded to only one surface site X. + + In this transition atom1 and atom4 are surface sites while atom2 and atom3 + are carbon or nitrogen atoms. + """ + cython.declare(paths=list, atom2=Vertex, atom3=Vertex, atom4=Vertex, bond12=Edge, bond23=Edge, bond34=Edge) + + paths = [] + if atom1.is_surface_site(): + for atom2, bond12 in atom1.edges.items(): + if atom2.is_carbon() or atom2.is_nitrogen(): + for atom3, bond23 in atom2.edges.items(): + if atom3.is_carbon() or atom3.is_nitrogen(): + for atom4, bond34 in atom3.edges.items(): + if atom4.is_surface_site(): + paths.append([atom1, atom2, atom3, atom4, bond12, bond23, bond34]) + return paths + + +def find_adsorbate_conjugate_delocalization_paths(atom1): + """ + Find all multidentate adsorbates which have a bonding configuration X-C-C-C-X. + Examples: + + - XCHCHXCH/XCHCHXC, where X is the surface site. The adsorption site X + is always placed on the left-hand side of the adatom and every adatom + is bonded to only one surface site X. + + In this transition atom1 and atom5 are surface sites while atom2, atom3, + and atom4 are carbon or nitrogen atoms. + """ + + cython.declare(paths=list, atom2=Vertex, atom3=Vertex, atom4=Vertex, atom5=Vertex, bond12=Edge, bond23=Edge, bond34=Edge, bond45=Edge) + + paths = [] + if atom1.is_surface_site(): + for atom2, bond12 in atom1.edges.items(): + if atom2.is_carbon() or atom2.is_nitrogen(): + for atom3, bond23 in atom2.edges.items(): + if atom3.is_carbon() or atom3.is_nitrogen(): + for atom4, bond34 in atom3.edges.items(): + if atom2 is not atom4 and (atom4.is_carbon() or atom4.is_nitrogen()): + for atom5, bond45 in atom4.edges.items(): + if atom5.is_surface_site(): + paths.append([atom1, atom2, atom3, atom4, atom5, bond12, bond23, bond34, bond45]) + return paths diff --git a/molecule/molecule/resonance.pxd b/molecule/molecule/resonance.pxd index ab3d3f8..a6bb7bd 100644 --- a/molecule/molecule/resonance.pxd +++ b/molecule/molecule/resonance.pxd @@ -63,3 +63,9 @@ cpdef list generate_clar_structures(Graph mol, bint save_order=?) cpdef list _clar_optimization(Graph mol, list constraints=?, max_num=?, save_order=?) cpdef list _clar_transformation(Graph mol, list aromatic_ring) + +cpdef list generate_adsorbate_shift_down_resonance_structures(Graph mol) + +cpdef list generate_adsorbate_shift_up_resonance_structures(Graph mol) + +cpdef list generate_adsorbate_conjugate_resonance_structures(Graph mol) \ No newline at end of file diff --git a/molecule/molecule/resonance.py b/molecule/molecule/resonance.py index 8dc4ca1..67dcf76 100644 --- a/molecule/molecule/resonance.py +++ b/molecule/molecule/resonance.py @@ -49,6 +49,10 @@ - ``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 +- Multidentate adsorbates only + - ``generate_adsorbate_shift_down_resonance_structures``: shift 2 electrons from a C=/#C bond to the X-C bond + - ``generate_adsorbate_shift_up_resonance_structures``: shift 2 electrons from a X=/#C bond to a C-C bond + - ``generate_adsorbate_conjugate_resonance_structures``: shift 2 electrons in a conjugate pi system for bridged X-C-C-C-X adsorbates """ import logging @@ -62,6 +66,7 @@ from molecule.molecule.graph import Vertex from molecule.molecule.kekulize import kekulize from molecule.molecule.molecule import Atom, Bond, Molecule +from molecule.molecule.fragment import CuttingLabel def populate_resonance_algorithms(features=None): @@ -86,6 +91,9 @@ def populate_resonance_algorithms(features=None): generate_aryne_resonance_structures, generate_kekule_structure, generate_clar_structures, + generate_adsorbate_shift_down_resonance_structures, + generate_adsorbate_shift_up_resonance_structures, + generate_adsorbate_conjugate_resonance_structures ] else: # If the molecule is aromatic, then radical resonance has already been considered @@ -109,7 +117,10 @@ def populate_resonance_algorithms(features=None): # solution. A more holistic approach would be to identify these cases in generate_resonance_structures, # and pass a list of forbidden atom ID's to find_lone_pair_multiple_bond_paths. method_list.append(generate_lone_pair_multiple_bond_resonance_structures) - + if features['is_multidentate']: + method_list.append(generate_adsorbate_shift_down_resonance_structures) + method_list.append(generate_adsorbate_shift_up_resonance_structures) + method_list.append(generate_adsorbate_conjugate_resonance_structures) return method_list @@ -129,6 +140,7 @@ def analyze_molecule(mol, save_order=False): 'is_aryl_radical': False, 'hasNitrogenVal5': False, 'hasLonePairs': False, + 'is_multidentate': mol.is_multidentate(), } if features['is_cyclic']: @@ -827,7 +839,7 @@ def generate_kekule_structure(mol): cython.declare(atom=Vertex, molecule=Graph) for atom in mol.atoms: - if not isinstance(atom, Atom): + if isinstance(atom,CuttingLabel): continue if atom.atomtype.label == 'Cb' or atom.atomtype.label == 'Cbf': break @@ -1116,3 +1128,111 @@ def _clar_transformation(mol, aromatic_ring): for bond in bond_list: bond.order = 1.5 + + +def generate_adsorbate_shift_down_resonance_structures(mol): + """ + Generate all of the resonance structures formed by the shift a pi bond between two C-C atoms to both X-C bonds. + Example XCHXCH: [X]C=C[X] <=> [X]=CC=[X] + (where '=' denotes a double bond) + """ + cython.declare(structures=list, paths=list, index=cython.int, structure=Graph) + cython.declare(atom=Vertex, atom1=Vertex, atom2=Vertex, atom3=Vertex, atom4=Vertex, bond12=Edge, bond23=Edge, bond34=Edge) + cython.declare(v1=Vertex, v2=Vertex) + + structures = [] + if mol.is_multidentate(): + for atom in mol.vertices: + paths = pathfinder.find_adsorbate_delocalization_paths(atom) + for atom1, atom2, atom3, atom4, bond12, bond23, bond34 in paths: + if bond23.is_single(): + continue + else: + bond12.increment_order() + bond23.decrement_order() + bond34.increment_order() + structure = mol.copy(deep=True) + bond12.decrement_order() + bond23.increment_order() + bond34.decrement_order() + try: + structure.update_atomtypes(log_species=False) + except AtomTypeError: + pass + else: + structures.append(structure) + return structures + + +def generate_adsorbate_shift_up_resonance_structures(mol): + """ + Generate all of the resonance structures formed by the shift of two electrons from X-C bonds to increase the bond + order between two C-C atoms by 1. + Example XCHXCH: [X]=CC=[X] <=> [X]C=C[X] + (where '=' denotes a double bond, '#' denotes a triple bond) + """ + cython.declare(structures=list, paths=list, index=cython.int, structure=Graph) + cython.declare(atom=Vertex, atom1=Vertex, atom2=Vertex, atom3=Vertex, atom4=Vertex, bond12=Edge, bond23=Edge, bond34=Edge) + cython.declare(v1=Vertex, v2=Vertex) + + structures = [] + if mol.is_multidentate(): + for atom in mol.vertices: + paths = pathfinder.find_adsorbate_delocalization_paths(atom) + for atom1, atom2, atom3, atom4, bond12, bond23, bond34 in paths: + if ((bond12.is_double_or_triple() and bond23.is_single() and bond34.is_double_or_triple()) or + (bond12.is_double() and bond23.is_double() and bond34.is_double())): + bond12.decrement_order() + bond23.increment_order() + bond34.decrement_order() + structure = mol.copy(deep=True) + bond12.increment_order() + bond23.decrement_order() + bond34.increment_order() + try: + structure.update_atomtypes(log_species=False) + except AtomTypeError: + pass + else: + structures.append(structure) + return structures + + +def generate_adsorbate_conjugate_resonance_structures(mol): + """ + Generate all of the resonance structures formed by the shift of two + electrons in a conjugated pi bond system of a bidentate adsorbate + with a bridging atom in between. + + Example XCHCHXC: [X]#CC=C[X] <=> [X]=C=CC=[X] + (where '#' denotes a triple bond, '=' denotes a double bond) + """ + cython.declare(structures=list, paths=list, index=cython.int, structure=Graph) + cython.declare(atom=Vertex, atom1=Vertex, atom2=Vertex, atom3=Vertex, atom4=Vertex, atom5=Vertex, bond12=Edge, bond23=Edge, bond34=Edge, bond45=Edge) + cython.declare(v1=Vertex, v2=Vertex) + + structures = [] + if mol.is_multidentate(): + for atom in mol.vertices: + paths = pathfinder.find_adsorbate_conjugate_delocalization_paths(atom) + for atom1, atom2, atom3, atom4, atom45, bond12, bond23, bond34, bond45 in paths: + if (bond12.is_double_or_triple() and + (bond23.is_single() or bond23.is_double()) and + bond34.is_double_or_triple() and + (bond45.is_single() or bond45.is_double())): + bond12.decrement_order() + bond23.increment_order() + bond34.decrement_order() + bond45.increment_order() + structure = mol.copy(deep=True) + bond12.increment_order() + bond23.decrement_order() + bond34.increment_order() + bond45.decrement_order() + try: + structure.update_atomtypes(log_species=False) + except AtomTypeError: + pass + else: + structures.append(structure) + return structures diff --git a/molecule/molecule/translator.py b/molecule/molecule/translator.py index 4694675..c2cb9ba 100644 --- a/molecule/molecule/translator.py +++ b/molecule/molecule/translator.py @@ -103,7 +103,17 @@ """ multiplicity 1 1 X u0 + """, + 'e': + """ + multiplicity 1 + 1 e u0 p0 c-1 + """, + '[H+]': """ + multiplicity 1 + 1 H u0 p0 c+1 + """, } #: This dictionary is used to shortcut lookups of a molecule's SMILES string from its chemical formula. @@ -128,6 +138,8 @@ 'ClH': 'Cl', 'I2': '[I][I]', 'HI': 'I', + 'H': 'H+', + 'e': 'e' } RADICAL_LOOKUPS = { @@ -155,7 +167,8 @@ 'I': '[I]', 'CF': '[C]F', 'CCl': '[C]Cl', - 'CBr': '[C]Br' + 'CBr': '[C]Br', + 'e': 'e' } @@ -169,7 +182,7 @@ def to_inchi(mol, backend='rdkit-first', aug_level=0): Uses RDKit or OpenBabel for conversion. Args: - backend choice of backend, 'try-all', 'rdkit', or 'openbabel' + backend choice of backend, 'rdkit-first' (default), 'openbabel-first', 'rdkit', or 'openbabel' aug_level level of augmentation, 0, 1, or 2 """ cython.declare(inchi=str, ulayer=str, player=str, mlayer=str) @@ -205,7 +218,7 @@ def to_inchi_key(mol, backend='rdkit-first', aug_level=0): Uses RDKit or OpenBabel for conversion. Args: - backend choice of backend, 'try-all', 'rdkit', or 'openbabel' + backend choice of backend, 'rdkit-first' (default), 'openbabel-first', 'rdkit', or 'openbabel' aug_level level of augmentation, 0, 1, or 2 """ cython.declare(key=str, ulayer=str, player=str, mlayer=str) @@ -274,11 +287,11 @@ def to_smiles(mol, backend='default'): return output -def from_inchi(mol, inchistr, backend='try-all', raise_atomtype_exception=True): +def from_inchi(mol, inchistr, backend='openbabel-first', raise_atomtype_exception=True): """ Convert an InChI string `inchistr` to a molecular structure. Uses - a user-specified backend for conversion, currently supporting - rdkit (default) and openbabel. + a user-specified backend for conversion, currently supporting 'openbabel-first' (default), rdkit-first, + rdkit, and openbabel. """ if inchiutil.INCHI_PREFIX in inchistr: return _read(mol, inchistr, 'inchi', backend, raise_atomtype_exception=raise_atomtype_exception) @@ -325,11 +338,11 @@ def from_smarts(mol, smartsstr, backend='rdkit', raise_atomtype_exception=True): return _read(mol, smartsstr, 'sma', backend, raise_atomtype_exception=raise_atomtype_exception) -def from_smiles(mol, smilesstr, backend='try-all', raise_atomtype_exception=True): +def from_smiles(mol, smilesstr, backend='openbabel-first', raise_atomtype_exception=True): """ Convert a SMILES string `smilesstr` to a molecular structure. Uses - a user-specified backend for conversion, currently supporting - rdkit (default) and openbabel. + a user-specified backend for conversion, currently supporting openbabel-first (default), rdkit-first, + rdkit and openbabel. """ return _read(mol, smilesstr, 'smi', backend, raise_atomtype_exception=raise_atomtype_exception) @@ -506,7 +519,7 @@ def _read(mol, identifier, identifier_type, backend, raise_atomtype_exception=Tr if _lookup(mol, identifier, identifier_type) is not None: if _check_output(mol, identifier): - mol.update_atomtypes(log_species=True, raise_exception=raise_atomtype_exception) + mol.update(log_species=True, raise_atomtype_exception=raise_atomtype_exception, sort_atoms=False) return mol for option in _get_backend_list(backend): @@ -518,7 +531,7 @@ def _read(mol, identifier, identifier_type, backend, raise_atomtype_exception=Tr raise NotImplementedError("Unrecognized backend {0}".format(option)) if _check_output(mol, identifier): - mol.update_atomtypes(log_species=True, raise_exception=raise_atomtype_exception) + mol.update(log_species=True, raise_atomtype_exception=raise_atomtype_exception, sort_atoms=False) return mol else: logging.debug('Backend {0} is not able to parse identifier {1}'.format(option, identifier)) @@ -569,9 +582,9 @@ def _get_backend_list(backend): """ if not isinstance(backend, str): raise ValueError("The backend argument should be a string. " - "Accepted values are 'try-all', 'rdkit-first', 'rdkit', and 'openbabel'") + "Accepted values are 'openbabel-first', 'rdkit-first', 'rdkit', and 'openbabel'") backend = backend.strip().lower() - if backend == 'try-all': + if backend == 'openbabel-first': return BACKENDS elif backend == 'rdkit-first': return reversed(BACKENDS) @@ -579,4 +592,4 @@ def _get_backend_list(backend): return [backend] else: raise ValueError("Unrecognized value for backend argument. " - "Accepted values are 'try-all', 'rdkit-first', 'rdkit', and 'openbabel'") + "Accepted values are 'openbabel-first', 'rdkit-first', 'rdkit', and 'openbabel'") diff --git a/molecule/pdep/network.py b/molecule/pdep/network.py index 81146bf..826c657 100644 --- a/molecule/pdep/network.py +++ b/molecule/pdep/network.py @@ -230,7 +230,7 @@ def initialize(self, Tmin, Tmax, Pmin, Pmax, maximum_grain_size=0.0, minimum_gra self.n_j = 0 # Calculate ground-state energies - self.E0 = np.zeros((self.n_isom + self.n_reac + self.n_prod), np.float64) + self.E0 = np.zeros((self.n_isom + self.n_reac + self.n_prod), float) for i in range(self.n_isom): self.E0[i] = self.isomers[i].E0 for n in range(self.n_reac): @@ -261,7 +261,7 @@ def calculate_rate_coefficients(self, Tlist, Plist, method, error_check=True, ne logging.debug('') logging.info('Calculating phenomenological rate coefficients for {0}...'.format(rxn)) - K = np.zeros((len(Tlist), len(Plist), n_isom + n_reac + n_prod, n_isom + n_reac + n_prod), np.float64) + K = np.zeros((len(Tlist), len(Plist), n_isom + n_reac + n_prod, n_isom + n_reac + n_prod), float) for t, T in enumerate(Tlist): for p, P in enumerate(Plist): @@ -381,10 +381,10 @@ def set_conditions(self, T, P, ymB=None): # Choose the angular momenta to use to compute k(T,P) values at this temperature # (This only applies if the J-rotor is adiabatic if not self.active_j_rotor: - j_list = self.j_list = np.arange(0, 20, 1, np.int) + j_list = self.j_list = np.arange(0, 20, 1, int) n_j = self.n_j = len(j_list) else: - j_list = self.j_list = np.array([0], np.int) + j_list = self.j_list = np.array([0], int) n_j = self.n_j = 1 # Map the densities of states onto this set of energies @@ -476,9 +476,9 @@ def _get_energy_grains(self, Emin, Emax, grain_size=0.0, grain_count=0): # Generate the array of energies if use_grain_size: - e_list = np.arange(Emin, Emax + grain_size, grain_size, dtype=np.float64) + e_list = np.arange(Emin, Emax + grain_size, grain_size, dtype=float) else: - e_list = np.linspace(Emin, Emax, grain_count, dtype=np.float64) + e_list = np.linspace(Emin, Emax, grain_count, dtype=float) return e_list @@ -559,7 +559,7 @@ def calculate_densities_of_states(self): # Shift the energy grains so that the minimum grain is zero e_list -= e_list[0] - dens_states = np.zeros((n_isom + n_reac + n_prod, n_grains), np.float64) + dens_states = np.zeros((n_isom + n_reac + n_prod, n_grains), float) # Densities of states for isomers for i in range(n_isom): @@ -652,9 +652,9 @@ def calculate_microcanonical_rates(self): n_prod = len(self.products) n_j = 1 if self.active_j_rotor else len(j_list) - self.Kij = np.zeros([n_isom, n_isom, n_grains, n_j], np.float64) - self.Gnj = np.zeros([n_reac + n_prod, n_isom, n_grains, n_j], np.float64) - self.Fim = np.zeros([n_isom, n_reac, n_grains, n_j], np.float64) + self.Kij = np.zeros([n_isom, n_isom, n_grains, n_j], float) + self.Gnj = np.zeros([n_reac + n_prod, n_isom, n_grains, n_j], float) + self.Fim = np.zeros([n_isom, n_reac, n_grains, n_j], float) isomers = [isomer.species[0] for isomer in self.isomers] reactants = [reactant.species for reactant in self.reactants] @@ -867,7 +867,7 @@ def calculate_equilibrium_ratios(self): n_isom = len(self.isomers) n_reac = len(self.reactants) n_prod = len(self.products) - eq_ratios = np.zeros(n_isom + n_reac + n_prod, np.float64) + eq_ratios = np.zeros(n_isom + n_reac + n_prod, float) conc = (1e5 / constants.R / temperature) # [=] mol/m^3 for i in range(n_isom): G = self.isomers[i].get_free_energy(temperature) @@ -894,8 +894,8 @@ def calculate_collision_model(self): n_j = 1 if self.j_list is None else len(self.j_list) try: - coll_freq = np.zeros(n_isom, np.float64) - m_coll = np.zeros((n_isom, n_grains, n_j, n_grains, n_j), np.float64) + coll_freq = np.zeros(n_isom, float) + m_coll = np.zeros((n_isom, n_grains, n_j, n_grains, n_j), float) except MemoryError: logging.warning('Collision matrix too large to manage') new_n_grains = int(n_grains / 2.0) diff --git a/molecule/qm/molecule.py b/molecule/qm/molecule.py index cea8bb1..84db72c 100644 --- a/molecule/qm/molecule.py +++ b/molecule/qm/molecule.py @@ -44,7 +44,7 @@ import molecule.qm.qmdata as qmdata import molecule.qm.symmetry as symmetry import molecule.quantity -#import molecule.statmech +# import molecule.statmech import molecule.thermo from molecule.qm.qmdata import parse_cclib_data from molecule.thermo import ThermoData @@ -408,19 +408,28 @@ def parse(self): parser = self.get_parser(self.output_file_path) parser.logger.setLevel( logging.ERROR - ) # cf. http://cclib.sourceforge.net/wiki/index.php/Using_cclib#Additional_information - parser.rotcons = ( + ) # cf. https://cclib.github.io/index.html#how-to-use-cclib + parser.molmass = None # give it an attribute and it won't delete it, leaving it on the parser object + parser.rotcons = ( # for cclib < 1.8.0 [] - ) # give it an attribute and it won't delete it, leaving it on the parser object - parser.molmass = None # give it an attribute and it won't delete it, leaving it on the parser object - cclib_data = parser.parse() + ) + parser.rotconsts = ( # for cclib >= 1.8.0 + [] + ) + cclib_data = parser.parse() # fills in either parser.rotcons or parser.rotconsts but not both + assert bool(parser.rotconsts) != bool(parser.rotcons) + if parser.rotcons: # for cclib < 1.8.0 + cclib_data.rotcons = ( + parser.rotcons + ) + else: # for cclib >= 1.8.0 + cclib_data.rotconsts = ( + parser.rotconsts + ) radical_number = self.molecule.get_radical_count() - cclib_data.rotcons = ( - parser.rotcons - ) # this hack required because rotcons not part of a default cclib data object cclib_data.molmass = ( parser.molmass - ) # this hack required because rotcons not part of a default cclib data object + ) # this hack required because molmass is not part of a default cclib data object qm_data = parse_cclib_data( cclib_data, radical_number + 1 ) # Should `radical_number+1` be `self.molecule.multiplicity` in the next line of code? It's the electronic ground state degeneracy. @@ -520,11 +529,11 @@ def load_thermo_data(self): self.qm_data = local_context["qmData"] return thermo - def get_augmented_inchi_key(self): + def get_augmented_inchi_key(self, backend='rdkit-first'): """ Returns the augmented InChI from self.molecule """ - return self.molecule.to_augmented_inchi_key() + return self.molecule.to_augmented_inchi_key(backend=backend) def get_mol_file_path_for_calculation(self, attempt): """ @@ -555,7 +564,7 @@ def calculate_chirality_correction(self): if self.point_group.chiral: return molecule.quantity.constants.R * math.log(2) else: - return 0. + return 0.0 # def calculate_thermo_data(self): # """ @@ -567,16 +576,21 @@ def calculate_chirality_correction(self): # assert self.qm_data, "Need QM Data first in order to calculate thermo." # assert self.point_group, "Need Point Group first in order to calculate thermo." # - # mass = getattr(self.qm_data, 'molecularMass', None) + # mass = getattr(self.qm_data, "molecularMass", None) # if mass is None: # # If using a cclib that doesn't read molecular mass, for example - # mass = sum(molecule.molecule.element.get_element(int(a)).mass for a in self.qm_data.atomicNumbers) - # mass = molecule.quantity.Mass(mass, 'kg/mol') + # mass = sum( + # molecule.molecule.element.get_element(int(a)).mass + # for a in self.qm_data.atomicNumbers + # ) + # mass = molecule.quantity.Mass(mass, "kg/mol") # trans = molecule.statmech.IdealGasTranslation(mass=mass) # if self.point_group.linear: # # there should only be one rotational constant for a linear rotor - # rotational_constant = molecule.quantity.Frequency(max(self.qm_data.rotationalConstants.value), - # self.qm_data.rotationalConstants.units) + # rotational_constant = molecule.quantity.Frequency( + # max(self.qm_data.rotationalConstants.value), + # self.qm_data.rotationalConstants.units, + # ) # rot = molecule.statmech.LinearRotor( # rotationalConstant=rotational_constant, # symmetry=self.point_group.symmetry_number, @@ -591,9 +605,11 @@ def calculate_chirality_correction(self): # # # @todo: We need to extract or calculate E0 somehow from the qmdata # E0 = (0, "kJ/mol") - # self.statesmodel = molecule.statmech.Conformer(E0=E0, - # modes=[trans, rot, vib], - # spin_multiplicity=self.qm_data.groundStateDegeneracy) + # self.statesmodel = molecule.statmech.Conformer( + # E0=E0, + # modes=[trans, rot, vib], + # spin_multiplicity=self.qm_data.groundStateDegeneracy, + # ) # # # we will use number of atoms from above (alternatively, we could use the chemGraph); this is needed to test whether the species is monoatomic # # SI units are J/mol, but converted to kJ/mol for generating the thermo. @@ -612,7 +628,7 @@ def calculate_chirality_correction(self): # S298=(S298, "J/(mol*K)"), # Tmin=(300.0, "K"), # Tmax=(2000.0, "K"), - # comment=comment + # comment=comment, # ) # self.thermo = thermo # return thermo diff --git a/molecule/qm/qmdata.py b/molecule/qm/qmdata.py index 4bda960..7af0afc 100644 --- a/molecule/qm/qmdata.py +++ b/molecule/qm/qmdata.py @@ -98,7 +98,11 @@ def parse_cclib_data(cclib_data, ground_state_degeneracy): molecular_mass = None energy = (cclib_data.scfenergies[-1], 'eV/molecule') atomic_numbers = cclib_data.atomnos - rotational_constants = (cclib_data.rotcons[-1], 'cm^-1') + if hasattr(cclib_data, 'rotconsts'): + rotational_constants = (cclib_data.rotconsts[-1], 'cm^-1') + else: + rotational_constants = (cclib_data.rotcons[-1], 'cm^-1') + atom_coords = (cclib_data.atomcoords[-1], 'angstrom') frequencies = (cclib_data.vibfreqs, 'cm^-1') diff --git a/molecule/quantity.py b/molecule/quantity.py index 5eb95ba..124a4e0 100644 --- a/molecule/quantity.py +++ b/molecule/quantity.py @@ -776,6 +776,8 @@ def __call__(self, *args, **kwargs): Momentum = UnitType('kg*m/s^2') +Potential = UnitType('V') + Power = UnitType('W') Pressure = UnitType('Pa', common_units=['bar', 'atm', 'torr', 'psi', 'mbar']) diff --git a/molecule/reaction.pxd b/molecule/reaction.pxd index a0b59b4..ae49d5e 100644 --- a/molecule/reaction.pxd +++ b/molecule/reaction.pxd @@ -49,8 +49,11 @@ cdef class Reaction: cdef public KineticsModel kinetics cdef public Arrhenius network_kinetics cdef public SurfaceArrhenius + # cdef public SurfaceChargeTransfer cdef public bint duplicate cdef public float _degeneracy + cdef public int electrons + cdef public int _protons cdef public list pairs cdef public bint allow_pdep_route cdef public bint elementary_high_p @@ -70,6 +73,10 @@ cdef class Reaction: cpdef bint is_surface_reaction(self) + cpdef bint is_charge_transfer_reaction(self) + + cpdef bint is_surface_charge_transfer_reaction(self) + cpdef bint has_template(self, list reactants, list products) cpdef bint matches_species(self, list reactants, list products=?) @@ -78,30 +85,56 @@ cdef class Reaction: bint check_only_label=?, bint check_template_rxn_products=?, bint generate_initial_map=?, bint strict=?, bint save_order=?) except -2 + cpdef double _apply_CHE_model(self, double T) + cpdef double get_enthalpy_of_reaction(self, double T) cpdef double get_entropy_of_reaction(self, double T) - cpdef double get_free_energy_of_reaction(self, double T) + cpdef double _get_free_energy_of_charge_transfer_reaction(self, double T, double potential=?) + + cpdef double get_free_energy_of_reaction(self, double T, double potential=?) - cpdef double get_equilibrium_constant(self, double T, str type=?, double surface_site_density=?) + cpdef double get_reversible_potential(self, double T) + + cpdef double set_reference_potential(self, double T) + + cpdef double get_equilibrium_constant(self, double T, double potential=?, str type=?, double surface_site_density=?) cpdef np.ndarray get_enthalpies_of_reaction(self, np.ndarray Tlist) cpdef np.ndarray get_entropies_of_reaction(self, np.ndarray Tlist) - cpdef np.ndarray get_free_energies_of_reaction(self, np.ndarray Tlist) + cpdef np.ndarray get_free_energies_of_reaction(self, np.ndarray Tlist, double potential=?) - cpdef np.ndarray get_equilibrium_constants(self, np.ndarray Tlist, str type=?) + cpdef np.ndarray get_equilibrium_constants(self, np.ndarray Tlist, double potential=?, str type=?) cpdef int get_stoichiometric_coefficient(self, Species spec) + cpdef double get_rate_coefficient(self, double T, double P=?, double surface_site_density=?, double potential=?) + + cpdef double get_surface_rate_coefficient(self, double T, double surface_site_density, double potential=?) except -2 + + cpdef fix_barrier_height(self, bint force_positive=?, str solvent=?, bint apply_solvation_correction=?) + + # cpdef reverse_arrhenius_rate(self, Arrhenius k_forward, str reverse_units, Tmin=?, Tmax=?) + + # cpdef reverse_surface_arrhenius_rate(self, SurfaceArrhenius k_forward, str reverse_units, Tmin=?, Tmax=?) + + # cpdef reverse_sticking_coeff_rate(self, StickingCoefficient k_forward, str reverse_units, double surface_site_density, Tmin=?, Tmax=?) + + # cpdef generate_reverse_rate_coefficient(self, bint network_kinetics=?, Tmin=?, Tmax=?, double surface_site_density=?) + cpdef np.ndarray calculate_tst_rate_coefficients(self, np.ndarray Tlist) cpdef double calculate_tst_rate_coefficient(self, double T) except -2 cpdef bint can_tst(self) except -2 + # cpdef calculate_microcanonical_rate_coefficient(self, np.ndarray e_list, np.ndarray j_list, + # np.ndarray reac_dens_states, np.ndarray prod_dens_states=?, + # double T=?) + cpdef bint is_balanced(self) cpdef generate_pairs(self) diff --git a/molecule/reaction.py b/molecule/reaction.py index bb600e7..29de47a 100644 --- a/molecule/reaction.py +++ b/molecule/reaction.py @@ -30,8 +30,8 @@ """ This module contains classes and functions for working with chemical reactions. -From the `IUPAC Compendium of Chemical Terminology -`_, a chemical reaction is "a process that +From the `IUPAC Compendium of Chemical Terminology +`_, a chemical reaction is "a process that results in the interconversion of chemical species". In RMG Py, a chemical reaction is represented in memory as a :class:`Reaction` @@ -53,14 +53,15 @@ from molecule.exceptions import ReactionError, KineticsError from molecule.kinetics import KineticsData, ArrheniusBM, ArrheniusEP, ThirdBody, Lindemann, Troe, Chebyshev, \ PDepArrhenius, MultiArrhenius, MultiPDepArrhenius, get_rate_coefficient_units_from_reaction_order, \ - SurfaceArrheniusBEP, StickingCoefficientBEP + SurfaceArrheniusBEP, StickingCoefficientBEP, ArrheniusChargeTransfer, ArrheniusChargeTransferBM, Marcus from molecule.kinetics.arrhenius import Arrhenius # Separate because we cimport from molecule.kinetics.arrhenius -from molecule.kinetics.surface import SurfaceArrhenius, StickingCoefficient # Separate because we cimport from molecule.kinetics.surface +from molecule.kinetics.surface import SurfaceArrhenius, StickingCoefficient, SurfaceChargeTransfer, SurfaceChargeTransferBEP # Separate because we cimport from molecule.kinetics.surface from molecule.kinetics.diffusionLimited import diffusion_limiter from molecule.molecule.element import Element, element_list from molecule.molecule.molecule import Molecule, Atom #from molecule.pdep.reaction import calculate_microcanonical_rate_coefficient from molecule.species import Species +from molecule.thermo import ThermoData ################################################################################ @@ -68,7 +69,7 @@ class Reaction: """ A chemical reaction. The attributes are: - + =================== =========================== ============================ Attribute Type Description =================== =========================== ============================ @@ -92,7 +93,7 @@ class Reaction: `is_forward` ``bool`` Indicates if the reaction was generated in the forward (true) or reverse (false) `rank` ``int`` Integer indicating the accuracy of the kinetics for this reaction =================== =========================== ============================ - + """ def __init__(self, @@ -110,10 +111,11 @@ def __init__(self, pairs=None, allow_pdep_route=False, elementary_high_p=False, - allow_max_rate_violation=False, rank=None, + electrons=0, comment='', is_forward=None, + allow_max_rate_violation=False, ): self.index = index self.label = label @@ -129,11 +131,12 @@ def __init__(self, self.pairs = pairs self.allow_pdep_route = allow_pdep_route self.elementary_high_p = elementary_high_p + self.rank = rank + self.electrons = electrons self.comment = comment self.k_effective_cache = {} self.is_forward = is_forward self.allow_max_rate_violation = allow_max_rate_violation - self.rank = rank def __repr__(self): """ @@ -157,7 +160,8 @@ def __repr__(self): if self.elementary_high_p: string += 'elementary_high_p={0}, '.format(self.elementary_high_p) if self.comment != '': string += 'comment={0!r}, '.format(self.comment) if self.rank is not None: string += 'rank={0!r},'.format(self.rank) - string = string[:-2] + ')' + if self.electrons != 0: string += 'electrons={0:d},'.format(self.electrons) + string = string[:-1] + ')' return string def __str__(self): @@ -169,7 +173,7 @@ def __str__(self): def to_labeled_str(self, use_index=False): """ - the same as __str__ except that the labels are assumed to exist and used for reactant and products rather than + the same as __str__ except that the labels are assumed to exist and used for reactant and products rather than the labels plus the index in parentheses """ arrow = ' <=> ' if self.reversible else ' => ' @@ -198,7 +202,8 @@ def __reduce__(self): self.allow_pdep_route, self.elementary_high_p, self.rank, - self.comment + self.electrons, + self.comment, )) @property @@ -231,10 +236,28 @@ def degeneracy(self, new): # set new degeneracy self._degeneracy = new + @property + def protons(self): + """ + The stochiometric coeff for protons in charge transfer reactions + """ + if self.is_charge_transfer_reaction(): + self._protons = 0 + for prod in self.products: + if prod.is_proton(): + self._protons += 1 + for react in self.reactants: + if react.is_proton(): + self._protons -= 1 + else: + self._protons = 0 + + return self._protons + def to_chemkin(self, species_list=None, kinetics=True): """ Return the chemkin-formatted string for this reaction. - + If `kinetics` is set to True, the chemkin format kinetics will also be returned (requires the `species_list` to figure out third body colliders.) Otherwise, only the reaction string will be returned. @@ -245,6 +268,7 @@ def to_chemkin(self, species_list=None, kinetics=True): else: return molecule.chemkin.write_reaction_string(self) + # def to_cantera(self, species_list=None, use_chemkin_identifier=False): # """ # Converts the RMG Reaction object to a Cantera Reaction object @@ -281,61 +305,109 @@ def to_chemkin(self, species_list=None, kinetics=True): # ct_products[product_name] += 1 # else: # ct_products[product_name] = 1 + # # if self.specific_collider: # add a specific collider if exists # ct_collider[self.specific_collider.to_chemkin() if use_chemkin_identifier else self.specific_collider.label] = 1 # - # # if self.kinetics: - # # if isinstance(self.kinetics, Arrhenius): - # # # Create an Elementary Reaction - # # ct_reaction = ct.ElementaryReaction(reactants=ct_reactants, products=ct_products) - # # elif isinstance(self.kinetics, MultiArrhenius): - # # # Return a list of elementary reactions which are duplicates - # # ct_reaction = [ct.ElementaryReaction(reactants=ct_reactants, products=ct_products) - # # for arr in self.kinetics.arrhenius] - # # - # # elif isinstance(self.kinetics, PDepArrhenius): - # # ct_reaction = ct.PlogReaction(reactants=ct_reactants, products=ct_products) - # # - # # elif isinstance(self.kinetics, MultiPDepArrhenius): - # # ct_reaction = [ct.PlogReaction(reactants=ct_reactants, products=ct_products) - # # for arr in self.kinetics.arrhenius] - # # - # # elif isinstance(self.kinetics, Chebyshev): - # # ct_reaction = ct.ChebyshevReaction(reactants=ct_reactants, products=ct_products) - # # - # # elif isinstance(self.kinetics, ThirdBody): - # # if ct_collider is not None: - # # ct_reaction = ct.ThreeBodyReaction(reactants=ct_reactants, products=ct_products, tbody=ct_collider) - # # else: - # # ct_reaction = ct.ThreeBodyReaction(reactants=ct_reactants, products=ct_products) - # # - # # elif isinstance(self.kinetics, Lindemann) or isinstance(self.kinetics, Troe): - # # if ct_collider is not None: - # # ct_reaction = ct.FalloffReaction(reactants=ct_reactants, products=ct_products, tbody=ct_collider) - # # else: - # # ct_reaction = ct.FalloffReaction(reactants=ct_reactants, products=ct_products) - # # else: - # # raise NotImplementedError('Unable to set cantera kinetics for {0}'.format(self.kinetics)) + # if not self.kinetics: + # raise Exception('Cantera reaction cannot be created because there was no kinetics.') + # + # # Create the Cantera reaction object, + # # with the correct type of kinetics object + # # but don't actually set its kinetics (we do that at the end) + # if isinstance(self.kinetics, Arrhenius): + # # Create an Elementary Reaction + # if isinstance(self.kinetics, SurfaceArrhenius): # SurfaceArrhenius inherits from Arrhenius + # ct_reaction = ct.Reaction(reactants=ct_reactants, products=ct_products, rate=ct.InterfaceArrheniusRate()) + # else: + # ct_reaction = ct.Reaction(reactants=ct_reactants, products=ct_products, rate=ct.ArrheniusRate()) + # elif isinstance(self.kinetics, MultiArrhenius): + # # Return a list of elementary reactions which are duplicates + # ct_reaction = [ct.Reaction(reactants=ct_reactants, products=ct_products, rate=ct.ArrheniusRate()) + # for arr in self.kinetics.arrhenius] + # + # elif isinstance(self.kinetics, PDepArrhenius): + # ct_reaction = ct.Reaction(reactants=ct_reactants, products=ct_products, rate=ct.PlogRate()) + # + # elif isinstance(self.kinetics, MultiPDepArrhenius): + # ct_reaction = [ct.Reaction(reactants=ct_reactants, products=ct_products, rate=ct.PlogRate()) + # for arr in self.kinetics.arrhenius] # - # # Set reversibility, duplicate, and ID attributes - # if isinstance(ct_reaction, list): - # for rxn in ct_reaction: - # rxn.reversible = self.reversible - # # Set the duplicate flag to true since this reaction comes from multiarrhenius or multipdeparrhenius - # rxn.duplicate = True - # # Set the ID flag to the original rmg index - # rxn.ID = str(self.index) + # elif isinstance(self.kinetics, Chebyshev): + # ct_reaction = ct.Reaction(reactants=ct_reactants, products=ct_products, rate=ct.ChebyshevRate()) + # + # elif isinstance(self.kinetics, ThirdBody): + # if ct_collider is not None: + # ct_reaction = ct.ThreeBodyReaction(reactants=ct_reactants, products=ct_products, third_body=ct_collider) + # else: + # ct_reaction = ct.ThreeBodyReaction(reactants=ct_reactants, products=ct_products) + # + # elif isinstance(self.kinetics, Troe): + # if ct_collider is not None: + # ct_reaction = ct.FalloffReaction( + # reactants=ct_reactants, + # products=ct_products, + # tbody=ct_collider, + # rate=ct.TroeRate() + # ) + # else: + # ct_reaction = ct.FalloffReaction( + # reactants=ct_reactants, + # products=ct_products, + # rate=ct.TroeRate() + # ) + # + # elif isinstance(self.kinetics, Lindemann): + # if ct_collider is not None: + # ct_reaction = ct.FalloffReaction( + # reactants=ct_reactants, + # products=ct_products, + # tbody=ct_collider, + # rate=ct.LindemannRate() + # ) # else: - # ct_reaction.reversible = self.reversible - # ct_reaction.duplicate = self.duplicate - # ct_reaction.ID = str(self.index) + # ct_reaction = ct.FalloffReaction( + # reactants=ct_reactants, + # products=ct_products, + # rate=ct.LindemannRate() + # ) # - # self.kinetics.set_cantera_kinetics(ct_reaction, species_list) + # elif isinstance(self.kinetics, SurfaceArrhenius): + # ct_reaction = ct.InterfaceReaction( + # reactants=ct_reactants, + # products=ct_products, + # rate=ct.InterfaceArrheniusRate() + # ) # - # return ct_reaction + # elif isinstance(self.kinetics, StickingCoefficient): + # ct_reaction = ct.Reaction( + # reactants=ct_reactants, + # products=ct_products, + # rate=ct.StickingArrheniusRate() + # ) # # else: - # raise Exception('Cantera reaction cannot be created because there was no kinetics.') + # raise NotImplementedError(f"Unable to set cantera kinetics for {self.kinetics}") + # + # # Set reversibility, duplicate, and ID attributes + # if isinstance(ct_reaction, list): + # for rxn in ct_reaction: + # rxn.reversible = self.reversible + # # Set the duplicate flag to true since this reaction comes from multiarrhenius or multipdeparrhenius + # rxn.duplicate = True + # # Set the ID flag to the original rmg index + # rxn.ID = str(self.index) + # else: + # ct_reaction.reversible = self.reversible + # ct_reaction.duplicate = self.duplicate + # ct_reaction.ID = str(self.index) + # + # # Now we set the kinetics. + # self.kinetics.set_cantera_kinetics(ct_reaction, species_list) + # + # return ct_reaction + + def get_url(self): """ @@ -397,6 +469,22 @@ def is_surface_reaction(self): return True return False + def is_charge_transfer_reaction(self): + """ + Return ``True`` if one or more reactants or products are electrons + """ + if self.electrons != 0: + return True + return False + + def is_surface_charge_transfer_reaction(self): + """ + Return ``True`` if one or more reactants or products are electrons + """ + if self.is_surface_reaction() and self.is_charge_transfer_reaction(): + return True + return False + def has_template(self, reactants, products): """ Return ``True`` if the reaction matches the template of `reactants` @@ -464,6 +552,10 @@ def is_isomorphic(self, other, either_direction=True, check_identical=False, che strict=strict, save_order=save_order) + # compare stoichiometry of electrons in reaction + if self.electrons != other.electrons: + return False + # Compare reactants to reactants forward_reactants_match = same_species_lists(self.reactants, other.reactants, check_identical=check_identical, @@ -508,6 +600,33 @@ def is_isomorphic(self, other, either_direction=True, check_identical=False, che # should have already returned if it matches forwards, or we're not allowed to match backwards return reverse_reactants_match and reverse_products_match and collider_match + def _apply_CHE_model(self, T): + """ + Apply the computational hydrogen electrode (CHE) model at temperature T (in 'K'). + + Returns the free energy (in J/mol) of 'N' proton/electron couple(s) in the reaction + using the Reversible Hydrogen Electrode (RHE) as referernce so that + N * deltaG(H+ + e-) = N * 1/2 deltaG(H2(g)) at 0V. + """ + + if not self.is_charge_transfer_reaction(): + raise ReactionError("CHE model is only applicable to charge transfer reactions!") + + if self.electrons != self.protons: + raise ReactionError("Number of electrons must equal number of protons! " + f"{self} has {self.electrons} protons and {self.electrons} electrons") + + + H2_thermo = ThermoData(Tdata=([300,400,500,600,800,1000,1500],'K'), + Cpdata=([6.895,6.975,6.994,7.009,7.081,7.219,7.72],'cal/(mol*K)'), + H298=(0,'kcal/mol'), S298=(31.233,'cal/(mol*K)','+|-',0.0007), + Cp0=(29.1007,'J/(mol*K)'), CpInf=(37.4151,'J/(mol*K)'), + label="""H2""", comment="""Thermo library: primaryThermoLibrary""") + # deltG_H+ + deltaG_e- -> 1/2 deltaG_H2 # only at 298K ??? + + return self.electrons * 0.5 * H2_thermo.get_free_energy(T) + + def get_enthalpy_of_reaction(self, T): """ Return the enthalpy of reaction in J/mol evaluated at temperature @@ -534,12 +653,40 @@ def get_entropy_of_reaction(self, T): dSrxn += product.get_entropy(T) return dSrxn - def get_free_energy_of_reaction(self, T): + def _get_free_energy_of_charge_transfer_reaction(self, T, potential=0.): + + cython.declare(dGrxn=cython.double, reactant=Species, product=Species) + + dGrxn = 0 + for reactant in self.reactants: + try: + dGrxn -= reactant.get_free_energy(T) + except Exception: + logging.error("Problem with reactant {!r} in reaction {!s}".format(reactant, self)) + raise + + for product in self.products: + try: + dGrxn += product.get_free_energy(T) + except Exception: + logging.error("Problem with product {!r} in reaction {!s}".format(reactant, self)) + raise + + if potential != 0.: + dGrxn -= self.electrons * constants.F * potential + + return dGrxn + + def get_free_energy_of_reaction(self, T, potential=0.): """ Return the Gibbs free energy of reaction in J/mol evaluated at - temperature `T` in K. + temperature `T` in K and potential in Volts (if applicable) """ cython.declare(dGrxn=cython.double, reactant=Species, product=Species) + + if self.is_charge_transfer_reaction(): + return self._get_free_energy_of_charge_transfer_reaction(T, potential=potential) + dGrxn = 0.0 for reactant in self.reactants: try: @@ -553,9 +700,33 @@ def get_free_energy_of_reaction(self, T): except Exception: logging.error("Problem with product {!r} in reaction {!s}".format(reactant, self)) raise + return dGrxn - def get_equilibrium_constant(self, T, type='Kc', surface_site_density=2.5e-05): + def get_reversible_potential(self, T): + """ + Get the Potential in `V` at T in 'K' at which the charge transfer reaction is at equilibrium + """ + cython.declare(deltaG=cython.double, V0=cython.double) + if not self.is_charge_transfer_reaction(): + raise KineticsError("Cannot get reversible potential for non charge transfer reactions") + + deltaG = self._get_free_energy_of_charge_transfer_reaction(T) #J/mol + V0 = deltaG / self.electrons / constants.F # V = deltaG / n / F + return V0 + + def set_reference_potential(self, T): + """ + Set the reference Potential of the `SurfaceChargeTransfer` kinetics model to the reversible potential + of the reaction + """ + if self.kinetics is None: + raise KineticsError("Cannot set reference potential for reactions with no kinetics attribute") + + if isinstance(self.kinetics, SurfaceChargeTransfer) and self.kinetics.V0 is None: + self.kinetics.V0 = (self.get_reversible_potential(T),'V') + + def get_equilibrium_constant(self, T, potential=0., type='Kc', surface_site_density=2.5e-05): """ Return the equilibrium constant for the reaction at the specified temperature `T` in K and reference `surface_site_density` @@ -564,11 +735,28 @@ def get_equilibrium_constant(self, T, type='Kc', surface_site_density=2.5e-05): ``Kc`` for concentrations (default), or ``Kp`` for pressures. This function assumes a reference pressure of 1e5 Pa for gas phases species and uses the ideal gas law to determine reference concentrations. For - surface species, the `surface_site_density` is the assumed reference. - """ - cython.declare(dGrxn=cython.double, K=cython.double, C0=cython.double, P0=cython.double) + surface species, the `surface_site_density` is the assumed reference. For protons (H+), + a reference concentration of 1000 mol/m^3 (1 mol/L) is assumed + """ + cython.declare( + dGrxn=cython.double, + K=cython.double, + C0=cython.double, + P0=cython.double, + dN_gas=cython.int, + dN_surf=cython.int, + sites=cython.int, + number_of_gas_reactants=cython.int, + number_of_gas_products=cython.int, + number_of_surface_reactants=cython.int, + number_of_surface_products=cython.int, + sigma_nu=cython.double, + rectant=Species, + product=Species, + spcs=Species, + ) # Use free energy of reaction to calculate Ka - dGrxn = self.get_free_energy_of_reaction(T) + dGrxn = self.get_free_energy_of_reaction(T, potential) K = np.exp(-dGrxn / constants.R / T) # Convert Ka to Kc or Kp if specified # Assume a pressure of 1e5 Pa for gas phase species @@ -579,9 +767,9 @@ def get_equilibrium_constant(self, T, type='Kc', surface_site_density=2.5e-05): number_of_gas_reactants = len([spcs for spcs in self.reactants if not spcs.contains_surface_site()]) number_of_gas_products = len([spcs for spcs in self.products if not spcs.contains_surface_site()]) except IndexError: - logging.warning("Species do not have an molecule.molecule.Molecule " - "Cannot determine phases of species. We will assume " - "ideal gas mixture when calculating Kc and Kp.") + #logging.warning("Species do not have an molecule.molecule.Molecule " + # "Cannot determine phases of species. We will assume " + # "ideal gas mixture when calculating Kc and Kp.") number_of_gas_reactants = len(self.reactants) number_of_gas_products = len(self.products) @@ -595,12 +783,34 @@ def get_equilibrium_constant(self, T, type='Kc', surface_site_density=2.5e-05): dN_gas = number_of_gas_products - number_of_gas_reactants # change in mols of gas spcs if type == 'Kc': + # Determine the multiplication factor of the binding site^(-stoichiometric coefficient) + # (only relevant for reactions involving multidentate adsorbates) + sigma_nu = 1 + # if there was a species with no molecule[0], then we would have presumed (above) + # that everything is gas phase, and this bit will skip. + if number_of_surface_products > 0: + for product in self.products: + sites = product.number_of_surface_sites() + if sites > 1: + # product has stoichiometric_coefficient > 0 + # so we need to divide by the number of surface sites + sigma_nu /= sites + if number_of_surface_reactants > 0: + for reactant in self.reactants: + sites = reactant.number_of_surface_sites() + if sites > 1: + # reactant has stoichiometric_coefficient < 0 + # so we need to multiply by the number of surface sites + sigma_nu *= sites + # Convert from Ka to Kc; C0 is the reference concentration if dN_gas: C0 = P0 / constants.R / T K *= C0 ** dN_gas if dN_surf: K *= surface_site_density ** dN_surf + if sigma_nu != 1: + K *= sigma_nu elif type == 'Kp': # Convert from Ka to Kp; P0 is the reference pressure K *= P0 ** dN_gas @@ -616,23 +826,23 @@ def get_enthalpies_of_reaction(self, Tlist): Return the enthalpies of reaction in J/mol evaluated at temperatures `Tlist` in K. """ - return np.array([self.get_enthalpy_of_reaction(T) for T in Tlist], np.float64) + return np.array([self.get_enthalpy_of_reaction(T) for T in Tlist], float) def get_entropies_of_reaction(self, Tlist): """ Return the entropies of reaction in J/mol*K evaluated at temperatures `Tlist` in K. """ - return np.array([self.get_entropy_of_reaction(T) for T in Tlist], np.float64) + return np.array([self.get_entropy_of_reaction(T) for T in Tlist], float) - def get_free_energies_of_reaction(self, Tlist): + def get_free_energies_of_reaction(self, Tlist, potential=0.): """ Return the Gibbs free energies of reaction in J/mol evaluated at temperatures `Tlist` in K. """ - return np.array([self.get_free_energy_of_reaction(T) for T in Tlist], np.float64) + return np.array([self.get_free_energy_of_reaction(T, potential=potential) for T in Tlist], float) - def get_equilibrium_constants(self, Tlist, type='Kc'): + def get_equilibrium_constants(self, Tlist, potential=0., type='Kc'): """ Return the equilibrium constants for the reaction at the specified temperatures `Tlist` in K. The `type` parameter lets you specify the @@ -640,7 +850,7 @@ def get_equilibrium_constants(self, Tlist, type='Kc'): ``Kc`` for concentrations (default), or ``Kp`` for pressures. Note that this function currently assumes an ideal gas mixture. """ - return np.array([self.get_equilibrium_constant(T, type) for T in Tlist], np.float64) + return np.array([self.get_equilibrium_constant(T, potential=potential, type=type) for T in Tlist], float) def get_stoichiometric_coefficient(self, spec): """ @@ -657,12 +867,12 @@ def get_stoichiometric_coefficient(self, spec): if product is spec: stoich += 1 return stoich - def get_rate_coefficient(self, T, P=0, surface_site_density=0): + def get_rate_coefficient(self, T, P=0, surface_site_density=0, potential=0.): """ Return the overall rate coefficient for the forward reaction at temperature `T` in K and pressure `P` in Pa, including any reaction path degeneracies. - + If diffusion_limiter is enabled, the reaction is in the liquid phase and we use a diffusion limitation to correct the rate. If not, then use the intrinsic rate coefficient. @@ -670,9 +880,11 @@ def get_rate_coefficient(self, T, P=0, surface_site_density=0): If the reaction has sticking coefficient kinetics, a nonzero surface site density in `mol/m^2` must be provided """ - if isinstance(self.kinetics, StickingCoefficient): + if isinstance(self.kinetics,SurfaceChargeTransfer): + return self.get_surface_rate_coefficient(T, surface_site_density=surface_site_density, potential=potential) + elif isinstance(self.kinetics, StickingCoefficient): if surface_site_density <= 0: - raise ValueError("Please provide a postive surface site density in mol/m^2 " + raise ValueError("Please provide a postive surface site density in mol/m^2 " f"for calculating the rate coefficient of {StickingCoefficient.__name__} kinetics") else: return self.get_surface_rate_coefficient(T, surface_site_density) @@ -686,14 +898,15 @@ def get_rate_coefficient(self, T, P=0, surface_site_density=0): else: return self.kinetics.get_rate_coefficient(T, P) - def get_surface_rate_coefficient(self, T, surface_site_density): + def get_surface_rate_coefficient(self, T, surface_site_density, potential=0.): """ Return the overall surface rate coefficient for the forward reaction at temperature `T` in K with surface site density `surface_site_density` in mol/m2. Value is returned in combination of [m,mol,s] """ cython.declare(rateCoefficient=cython.double, - molecularWeight_kg=cython.double, ) + molecularWeight_kg=cython.double, + Ea=cython.double, deltaG=cython.double) if diffusion_limiter.enabled: raise NotImplementedError() @@ -706,6 +919,9 @@ def get_surface_rate_coefficient(self, T, surface_site_density): for r in self.reactants: if r.contains_surface_site(): rate_coefficient /= surface_site_density + sites = r.number_of_surface_sites() + if sites > 1: + rate_coefficient /= sites else: if adsorbate is None: adsorbate = r @@ -721,12 +937,32 @@ def get_surface_rate_coefficient(self, T, surface_site_density): # molecular_weight_kg in kg per molecule rate_coefficient *= math.sqrt(constants.kB * T / (2 * math.pi * molecular_weight_kg)) - # ToDo: missing the sigma terms for bidentate species. only works for single site adsorption + # Multidentate adsorption requires multiplication of the sticking coefficient + # with the number of binding sites**stoichiometric coefficients (it is 1 for monodentates) + # Integrated in the loop above for reactants + for p in self.products: + sites = p.number_of_surface_sites() + if sites > 1: + rate_coefficient *= sites + return rate_coefficient if isinstance(self.kinetics, SurfaceArrhenius): return self.kinetics.get_rate_coefficient(T, P=0) + if isinstance(self.kinetics, SurfaceChargeTransfer): + Ea = self.kinetics.get_activation_energy_from_potential(potential) + deltaG = self._get_free_energy_of_charge_transfer_reaction(298,potential) + if deltaG > 0 and Ea < deltaG: + corrected_kinetics = deepcopy(self.kinetics) + corrected_kinetics.V0.value_si = potential + corrected_kinetics.Ea.value_si = deltaG + logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol at {3:.2f} V".format( + self, self.kinetics.Ea.value_si / 1000., deltaG / 1000., potential)) + return corrected_kinetics.get_rate_coefficient(T, potential) + else: + return self.kinetics.get_rate_coefficient(T, potential) + raise NotImplementedError("Can't get_surface_rate_coefficient for kinetics type {!r}".format(type(self.kinetics))) def fix_diffusion_limited_a_factor(self, T): @@ -753,26 +989,29 @@ def fix_diffusion_limited_a_factor(self, T): "diffusion factor {0.2g} evaluated at {1} K.").format( diffusion_factor, T)) - def fix_barrier_height(self, force_positive=False): + def fix_barrier_height(self, force_positive=False, solvent="", apply_solvation_correction=True): """ Turns the kinetics into Arrhenius (if they were ArrheniusEP) and ensures the activation energy is at least the endothermicity - for endothermic reactions, and is not negative only as a result + for endothermic reactions, and is not negative only as a result of using Evans Polanyi with an exothermic reaction. If `force_positive` is True, then all reactions are forced to have a non-negative barrier. """ - cython.declare(H0=cython.double, H298=cython.double, Ea=cython.double) + cython.declare(H0=cython.double, H298=cython.double, Ea=cython.double, V0=cython.double, + deltaG=cython.double) if self.kinetics is None: raise KineticsError("Cannot fix barrier height for reactions with no kinetics attribute") - H298 = self.get_enthalpy_of_reaction(298) - H0 = sum([spec.get_thermo_data().E0.value_si for spec in self.products]) \ - - sum([spec.get_thermo_data().E0.value_si for spec in self.reactants]) - if isinstance(self.kinetics, (ArrheniusEP, SurfaceArrheniusBEP, StickingCoefficientBEP, ArrheniusBM)): + if isinstance(self.kinetics, Marcus): + if apply_solvation_correction and solvent: + self.apply_solvent_correction(solvent) + elif isinstance(self.kinetics, SurfaceChargeTransferBEP): Ea = self.kinetics.E0.value_si # temporarily using Ea to store the intrinsic barrier height E0 - self.kinetics = self.kinetics.to_arrhenius(H298) + V0 = self.kinetics.V0.value_si + deltaG = self._get_free_energy_of_charge_transfer_reaction(298,V0) + self.kinetics = self.kinetics.to_surface_charge_transfer(deltaG) if self.kinetics.Ea.value_si < 0.0 and self.kinetics.Ea.value_si < Ea: # Calculated Ea (from Evans-Polanyi) is negative AND below than the intrinsic E0 Ea = min(0.0, Ea) # (the lowest we want it to be) @@ -781,33 +1020,67 @@ def fix_barrier_height(self, force_positive=False): logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol.".format( self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) self.kinetics.Ea.value_si = Ea - if isinstance(self.kinetics, (Arrhenius, StickingCoefficient)): # SurfaceArrhenius is a subclass of Arrhenius - Ea = self.kinetics.Ea.value_si - if H0 >= 0 and Ea < H0: - self.kinetics.Ea.value_si = H0 - self.kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol to match endothermicity of " \ - "reaction.".format( Ea / 1000., H0 / 1000.) - logging.info("For reaction {2!s}, Ea raised from {0:.1f} to {1:.1f} kJ/mol to match " - "endothermicity of reaction.".format( Ea / 1000., H0 / 1000., self)) - if force_positive and isinstance(self.kinetics, (Arrhenius, StickingCoefficient)) and self.kinetics.Ea.value_si < 0: - self.kinetics.comment += "\nEa raised from {0:.1f} to 0 kJ/mol.".format(self.kinetics.Ea.value_si / 1000.) - logging.info("For reaction {1!s} Ea raised from {0:.1f} to 0 kJ/mol.".format( - self.kinetics.Ea.value_si / 1000., self)) - self.kinetics.Ea.value_si = 0 - if self.kinetics.is_pressure_dependent() and self.network_kinetics is not None: - Ea = self.network_kinetics.Ea.value_si - if H0 >= 0 and Ea < H0: - self.network_kinetics.Ea.value_si = H0 - self.network_kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol to match endothermicity of" \ - " reaction.".format(Ea / 1000., H0 / 1000.) - logging.info("For reaction {2!s}, Ea of the high pressure limit kinetics raised from {0:.1f} to {1:.1f}" - " kJ/mol to match endothermicity of reaction.".format(Ea / 1000., H0 / 1000., self)) - if force_positive and isinstance(self.kinetics, Arrhenius) and self.kinetics.Ea.value_si < 0: - self.network_kinetics.comment += "\nEa raised from {0:.1f} to 0 kJ/mol.".format( - self.kinetics.Ea.value_si / 1000.) - logging.info("For reaction {1!s} Ea of the high pressure limit kinetics raised from {0:.1f} to 0" - " kJ/mol.".format(self.kinetics.Ea.value_si / 1000., self)) + else: + H298 = self.get_enthalpy_of_reaction(298) + H0 = sum([spec.get_thermo_data().E0.value_si if spec.get_thermo_data().E0 is not None else spec.get_thermo_data().to_wilhoit().E0.value_si for spec in self.products]) \ + - sum([spec.get_thermo_data().E0.value_si if spec.get_thermo_data().E0 is not None else spec.get_thermo_data().to_wilhoit().E0.value_si for spec in self.reactants]) + if isinstance(self.kinetics, (ArrheniusEP, SurfaceArrheniusBEP, StickingCoefficientBEP, ArrheniusBM)): + Ea = self.kinetics.E0.value_si # temporarily using Ea to store the intrinsic barrier height E0 + self.kinetics = self.kinetics.to_arrhenius(H298) + if self.kinetics.Ea.value_si < 0.0 and self.kinetics.Ea.value_si < Ea: + # Calculated Ea (from Evans-Polanyi) is negative AND below than the intrinsic E0 + Ea = min(0.0, Ea) # (the lowest we want it to be) + self.kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol.".format( + self.kinetics.Ea.value_si / 1000., Ea / 1000.) + logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol.".format( + self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) + self.kinetics.Ea.value_si = Ea + if isinstance(self.kinetics, ArrheniusChargeTransferBM): + Ea = self.kinetics.E0.value_si # temporarily using Ea to store the intrinsic barrier height E0 + self.kinetics = self.kinetics.to_arrhenius_charge_transfer(H298) + if self.kinetics.Ea.value_si < 0.0 and self.kinetics.Ea.value_si < Ea: + # Calculated Ea (from Evans-Polanyi) is negative AND below than the intrinsic E0 + Ea = min(0.0, Ea) # (the lowest we want it to be) + self.kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol.".format( + self.kinetics.Ea.value_si / 1000., Ea / 1000.) + logging.info("For reaction {0!s} Ea raised from {1:.1f} to {2:.1f} kJ/mol.".format( + self, self.kinetics.Ea.value_si / 1000., Ea / 1000.)) + self.kinetics.Ea.value_si = Ea + if isinstance(self.kinetics, (Arrhenius, StickingCoefficient, ArrheniusChargeTransfer, SurfaceChargeTransfer)): # SurfaceArrhenius is a subclass of Arrhenius + if apply_solvation_correction and solvent and self.kinetics.solute: + self.apply_solvent_correction(solvent) + Ea = self.kinetics.Ea.value_si + if H0 >= 0 and Ea < H0: + self.kinetics.Ea.value_si = H0 + self.kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol to match endothermicity of " \ + "reaction.".format( Ea / 1000., H0 / 1000.) + logging.info("For reaction {2!s}, Ea raised from {0:.1f} to {1:.1f} kJ/mol to match " + "endothermicity of reaction.".format( Ea / 1000., H0 / 1000., self)) + if force_positive and isinstance(self.kinetics, (Arrhenius, StickingCoefficient)) and self.kinetics.Ea.value_si < 0: + self.kinetics.comment += "\nEa raised from {0:.1f} to 0 kJ/mol.".format(self.kinetics.Ea.value_si / 1000.) + logging.info("For reaction {1!s} Ea raised from {0:.1f} to 0 kJ/mol.".format( + self.kinetics.Ea.value_si / 1000., self)) self.kinetics.Ea.value_si = 0 + if self.kinetics.is_pressure_dependent() and self.network_kinetics is not None: + Ea = self.network_kinetics.Ea.value_si + if H0 >= 0 and Ea < H0: + self.network_kinetics.Ea.value_si = H0 + self.network_kinetics.comment += "\nEa raised from {0:.1f} to {1:.1f} kJ/mol to match endothermicity of" \ + " reaction.".format(Ea / 1000., H0 / 1000.) + logging.info("For reaction {2!s}, Ea of the high pressure limit kinetics raised from {0:.1f} to {1:.1f}" + " kJ/mol to match endothermicity of reaction.".format(Ea / 1000., H0 / 1000., self)) + if force_positive and isinstance(self.kinetics, Arrhenius) and self.kinetics.Ea.value_si < 0: + self.network_kinetics.comment += "\nEa raised from {0:.1f} to 0 kJ/mol.".format( + self.kinetics.Ea.value_si / 1000.) + logging.info("For reaction {1!s} Ea of the high pressure limit kinetics raised from {0:.1f} to 0" + " kJ/mol.".format(self.kinetics.Ea.value_si / 1000., self)) + self.kinetics.Ea.value_si = 0 + + def apply_solvent_correction(self, solvent): + """ + apply kinetic solvent correction + """ + return NotImplementedError("solvent correction is particular to library, depository and template reactions") # def reverse_arrhenius_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): # """ @@ -829,6 +1102,7 @@ def fix_barrier_height(self, force_positive=False): # klist[i] = kf.get_rate_coefficient(Tlist[i]) / self.get_equilibrium_constant(Tlist[i]) # kr = Arrhenius() # kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + # kr.solute = kf.solute # return kr # # def reverse_surface_arrhenius_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): @@ -852,6 +1126,7 @@ def fix_barrier_height(self, force_positive=False): # klist[i] = kf.get_rate_coefficient(Tlist[i]) / self.get_equilibrium_constant(Tlist[i]) # kr = SurfaceArrhenius() # kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + # kr.solute = kf.solute # return kr # # def reverse_sticking_coeff_rate(self, k_forward, reverse_units, surface_site_density, Tmin=None, Tmax=None): @@ -878,6 +1153,57 @@ def fix_barrier_height(self, force_positive=False): # self.get_equilibrium_constant(Tlist[i], surface_site_density=surface_site_density) # kr = SurfaceArrhenius() # kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + # kr.solute = kf.solute + # return kr + # + # def reverse_surface_charge_transfer_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): + # """ + # Reverses the given k_forward, which must be a SurfaceChargeTransfer type. + # You must supply the correct units for the reverse rate. + # The equilibrium constant is evaluated from the current reaction instance (self). + # """ + # cython.declare(kf=SurfaceChargeTransfer, kr=SurfaceChargeTransfer) + # cython.declare(Tlist=np.ndarray, klist=np.ndarray, i=cython.int, V0=cython.double) + # kf = k_forward + # self.set_reference_potential(298) + # if not isinstance(kf, SurfaceChargeTransfer): # Only reverse SurfaceChargeTransfer rates + # raise TypeError(f'Expected a SurfaceChargeTransfer object for k_forward but received {kf}') + # if Tmin is not None and Tmax is not None: + # Tlist = 1.0 / np.linspace(1.0 / Tmax.value, 1.0 / Tmin.value, 50) + # else: + # Tlist = np.linspace(298, 500, 30) + # + # V0 = self.kinetics.V0.value_si + # klist = np.zeros_like(Tlist) + # for i in range(len(Tlist)): + # klist[i] = kf.get_rate_coefficient(Tlist[i],V0) / self.get_equilibrium_constant(Tlist[i],V0) + # kr = SurfaceChargeTransfer(alpha=kf.alpha.value, electrons=-1*self.electrons, V0=(V0,'V')) + # kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + # kr.solute = kf.solute + # return kr + # + # def reverse_arrhenius_charge_transfer_rate(self, k_forward, reverse_units, Tmin=None, Tmax=None): + # """ + # Reverses the given k_forward, which must be a SurfaceChargeTransfer type. + # You must supply the correct units for the reverse rate. + # The equilibrium constant is evaluated from the current reaction instance (self). + # """ + # cython.declare(Tlist=np.ndarray, klist=np.ndarray, i=cython.int, V0=cython.double) + # kf = k_forward + # if not isinstance(kf, ArrheniusChargeTransfer): # Only reverse SurfaceChargeTransfer rates + # raise TypeError(f'Expected a ArrheniusChargeTransfer object for k_forward but received {kf}') + # if Tmin is not None and Tmax is not None: + # Tlist = 1.0 / np.linspace(1.0 / Tmax.value, 1.0 / Tmin.value, 50) + # else: + # Tlist = np.linspace(298, 500, 30) + # + # V0 = self.kinetics.V0.value_si + # klist = np.zeros_like(Tlist) + # for i in range(len(Tlist)): + # klist[i] = kf.get_rate_coefficient(Tlist[i],V0) / self.get_equilibrium_constant(Tlist[i],V0) + # kr = ArrheniusChargeTransfer(alpha=kf.alpha.value, electrons=-1*self.electrons, V0=(V0,'V')) + # kr.fit_to_data(Tlist, klist, reverse_units, kf.T0.value_si) + # kr.solute = kf.solute # return kr # # def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, Tmax=None, surface_site_density=0): @@ -889,7 +1215,8 @@ def fix_barrier_height(self, force_positive=False): # If the reaction kinetics model is Sticking Coefficient, please provide a nonzero # surface site density in `mol/m^2` which is required to evaluate the rate coefficient. # """ - # cython.declare(Tlist=np.ndarray, Plist=np.ndarray, K=np.ndarray, + # cython.declare(n_gas=cython.int, n_surf=cython.int, prod=Species, k_units=str, + # Tlist=np.ndarray, Plist=np.ndarray, K=np.ndarray, # rxn=Reaction, klist=np.ndarray, i=cython.size_t, # Tindex=cython.size_t, Pindex=cython.size_t) # @@ -897,6 +1224,7 @@ def fix_barrier_height(self, force_positive=False): # KineticsData.__name__, # Arrhenius.__name__, # SurfaceArrhenius.__name__, + # SurfaceChargeTransfer.__name__, # MultiArrhenius.__name__, # PDepArrhenius.__name__, # MultiPDepArrhenius.__name__, @@ -905,6 +1233,7 @@ def fix_barrier_height(self, force_positive=False): # Lindemann.__name__, # Troe.__name__, # StickingCoefficient.__name__, + # ArrheniusChargeTransfer.__name__, # ) # # # Get the units for the reverse rate coefficient @@ -920,7 +1249,14 @@ def fix_barrier_height(self, force_positive=False): # kunits = get_rate_coefficient_units_from_reaction_order(n_gas, n_surf) # # kf = self.kinetics - # if isinstance(kf, KineticsData): + # + # if isinstance(kf, SurfaceChargeTransfer): + # return self.reverse_surface_charge_transfer_rate(kf, kunits, Tmin, Tmax) + # + # elif isinstance(kf, ArrheniusChargeTransfer): + # return self.reverse_arrhenius_charge_transfer_rate(kf, kunits, Tmin, Tmax) + # + # elif isinstance(kf, KineticsData): # # Tlist = kf.Tdata.value_si # klist = np.zeros_like(Tlist) @@ -949,15 +1285,15 @@ def fix_barrier_height(self, force_positive=False): # return self.reverse_arrhenius_rate(kf, kunits) # # elif isinstance(kf, Chebyshev): - # Tlist = 1.0 / np.linspace(1.0 / kf.Tmax.value, 1.0 / kf.Tmin.value, 50) - # Plist = np.linspace(kf.Pmin.value, kf.Pmax.value, 20) - # K = np.zeros((len(Tlist), len(Plist)), np.float64) + # Tlist = 1.0 / np.linspace(1.0 / kf.Tmax.value_si, 1.0 / kf.Tmin.value_si, 50) + # Plist = np.linspace(kf.Pmin.value_si, kf.Pmax.value_si, 20) + # K = np.zeros((len(Tlist), len(Plist)), float) # for Tindex, T in enumerate(Tlist): # for Pindex, P in enumerate(Plist): # K[Tindex, Pindex] = kf.get_rate_coefficient(T, P) / self.get_equilibrium_constant(T) # kr = Chebyshev() - # kr.fit_to_data(Tlist, Plist, K, kunits, kf.degreeT, kf.degreeP, kf.Tmin.value, kf.Tmax.value, kf.Pmin.value, - # kf.Pmax.value) + # kr.fit_to_data(Tlist, Plist, K, kunits, kf.degreeT, kf.degreeP, kf.Tmin.value, kf.Tmax.value, + # kf.Pmin.value_si, kf.Pmax.value_si) # return kr # # elif isinstance(kf, PDepArrhenius): @@ -1015,7 +1351,7 @@ def fix_barrier_height(self, force_positive=False): # "should be one of {1}".format(self.kinetics.__class__, supported_types)) def calculate_tst_rate_coefficients(self, Tlist): - return np.array([self.calculate_tst_rate_coefficient(T) for T in Tlist], np.float64) + return np.array([self.calculate_tst_rate_coefficient(T) for T in Tlist], float) def calculate_tst_rate_coefficient(self, T): """ @@ -1093,10 +1429,13 @@ def is_balanced(self): from molecule.molecule.element import element_list from molecule.molecule.fragment import CuttingLabel, Fragment - cython.declare(reactant_elements=dict, product_elements=dict, molecule=Graph, atom=Vertex, element=Element) + cython.declare(reactant_elements=dict, product_elements=dict, molecule=Graph, atom=Vertex, element=Element, + reactants_net_charge=cython.int, products_net_charge=cython.int) reactant_elements = {} product_elements = {} + reactants_net_charge = 0 + products_net_charge = 0 for element in element_list: reactant_elements[element] = 0 product_elements[element] = 0 @@ -1106,34 +1445,43 @@ def is_balanced(self): molecule = reactant.molecule[0] for atom in molecule.atoms: if not isinstance(atom, CuttingLabel): + reactants_net_charge += atom.charge reactant_elements[atom.element] += 1 - elif isinstance(reactant, Molecule): - molecule = reactant - for atom in molecule.atoms: - reactant_elements[atom.element] += 1 elif isinstance(reactant, Fragment): for atom in reactant.atoms: if not isinstance(atom, CuttingLabel): + reactants_net_charge += atom.charge reactant_elements[atom.element] += 1 + elif isinstance(reactant, Molecule): + for atom in reactant.atoms: + reactants_net_charge += atom.charge + reactant_elements[atom.element] += 1 for product in self.products: if isinstance(product, Species): molecule = product.molecule[0] for atom in molecule.atoms: if not isinstance(atom, CuttingLabel): + products_net_charge += atom.charge product_elements[atom.element] += 1 - elif isinstance(product, Molecule): - molecule = product - for atom in molecule.atoms: - product_elements[atom.element] += 1 elif isinstance(product, Fragment): for atom in product.atoms: if not isinstance(atom, CuttingLabel): + products_net_charge += atom.charge product_elements[atom.element] += 1 + elif isinstance(product, Molecule): + for atom in product.atoms: + products_net_charge += atom.charge + product_elements[atom.element] += 1 for element in element_list: if reactant_elements[element] != product_elements[element]: return False + if self.electrons < 0: + reactants_net_charge += self.electrons + elif self.electrons > 0: + products_net_charge -= self.electrons + return True def generate_pairs(self): @@ -1141,7 +1489,7 @@ def generate_pairs(self): Generate the reactant-product pairs to use for this reaction when performing flux analysis. The exact procedure for doing so depends on the reaction type: - + =================== =============== ======================================== Reaction type Template Resulting pairs =================== =============== ======================================== @@ -1150,8 +1498,8 @@ def generate_pairs(self): Association A + B -> C (A,C), (B,C) Bimolecular A + B -> C + D (A,C), (B,D) *or* (A,D), (B,C) =================== =============== ======================================== - - There are a number of ways of determining the correct pairing for + + There are a number of ways of determining the correct pairing for bimolecular reactions. Here we try a simple similarity analysis by comparing the number of heavy atoms. This should work most of the time, but a more rigorous algorithm may be needed for some cases. @@ -1214,9 +1562,9 @@ def _repr_png_(self): # Build the transition state geometry def generate_3d_ts(self, reactants, products): """ - Generate the 3D structure of the transition state. Called from + Generate the 3D structure of the transition state. Called from model.generate_kinetics(). - + self.reactants is a list of reactants self.products is a list of products """ @@ -1226,7 +1574,7 @@ def generate_3d_ts(self, reactants, products): atoms involved in the reaction. If a radical is involved, can find the atom with radical electrons. If a more reliable method can be found, would greatly improve the method. - + Repeat for the products """ for i in range(0, len(reactants)): diff --git a/molecule/rmg/pdep.py b/molecule/rmg/pdep.py index 1c27463..52d9f46 100644 --- a/molecule/rmg/pdep.py +++ b/molecule/rmg/pdep.py @@ -454,7 +454,7 @@ def solve_ss_network(self, T, P): if any(c <= 0.0): c, rnorm = opt.nnls(A, b) - c = c.astype(np.float64) + c = c.astype(float) except: # fall back to raw flux analysis rather than solve steady state problem return None diff --git a/molecule/species.pxd b/molecule/species.pxd index c455f3e..0244123 100644 --- a/molecule/species.pxd +++ b/molecule/species.pxd @@ -61,7 +61,9 @@ cdef class Species: cdef str _smiles cpdef generate_resonance_structures(self, bint keep_isomorphic=?, bint filter_structures=?, bint save_order=?) - + + cpdef get_net_charge(self) + cpdef bint is_isomorphic(self, other, bint generate_initial_map=?, bint save_order=?, bint strict=?) except -2 cpdef bint is_identical(self, other, bint strict=?) except -2 @@ -78,6 +80,10 @@ cdef class Species: cpdef bint is_surface_site(self) except -2 + cpdef bint is_electron(self) except -2 + + cpdef bint is_proton(self) except -2 + cpdef bint has_statmech(self) except -2 cpdef bint has_thermo(self) except -2 diff --git a/molecule/species.py b/molecule/species.py index e402301..7921aa7 100644 --- a/molecule/species.py +++ b/molecule/species.py @@ -278,6 +278,14 @@ def molecular_weight(self): def molecular_weight(self, value): self._molecular_weight = quantity.Mass(value) + def get_net_charge(self): + """ + Iterate through the atoms in the structure and calculate the net charge + on the overall molecule. + """ + + return self.molecule[0].get_net_charge() + def generate_resonance_structures(self, keep_isomorphic=True, filter_structures=True, save_order=False): """ Generate all of the resonance structures of this species. The isomers are @@ -358,15 +366,24 @@ def is_structure_in_list(self, species_list): ' should be a List of Species objects.'.format(species)) return False - def from_adjacency_list(self, adjlist, raise_atomtype_exception=True, raise_charge_exception=True): + def from_adjacency_list(self, adjlist, raise_atomtype_exception=True, raise_charge_exception=False): """ Load the structure of a species as a :class:`Molecule` object from the given adjacency list `adjlist` and store it as the first entry of a list in the `molecule` attribute. Does not generate resonance isomers of the loaded molecule. """ + lines = adjlist.splitlines() + + if len(lines[0].split()) == 1: + label = lines.pop(0) # remove the first line if it is a label before detecting cutting label + adjlist_no_label = '\n'.join(lines) + else: + adjlist_no_label = adjlist + # detect if it contains cutting label - _ , cutting_label_list = Fragment().detect_cutting_label(adjlist) + _ , cutting_label_list = Fragment().detect_cutting_label(adjlist_no_label) + if cutting_label_list == []: self.molecule = [Molecule().from_adjacency_list(adjlist, saturate_h=False, raise_atomtype_exception=raise_atomtype_exception, @@ -434,9 +451,19 @@ def to_cantera(self, use_chemkin_identifier=False): else: element_dict[symbol] += 1 if use_chemkin_identifier: - ct_species = ct.Species(self.to_chemkin(), element_dict) + label = self.to_chemkin() + else: + label = self.label + + if self.contains_surface_site() and element_dict["X"] > 1: + # for multidentate adsorbates, 'size' is the same as 'sites'? + # for some reason,cantera won't take the input 'sites' so will need to use 'size' + ct_species = ct.Species(label, element_dict, size=element_dict["X"]) + # hopefully this will be fixed soon, so that ct.Species can take a 'sites' parameter + # or that cantera can read input files with 'size' specified else: - ct_species = ct.Species(self.label, element_dict) + ct_species = ct.Species(label, element_dict) + if self.thermo: try: ct_species.thermo = self.thermo.to_cantera() @@ -479,6 +506,29 @@ def is_surface_site(self): """Return ``True`` if the species is a vacant surface site.""" return self.molecule[0].is_surface_site() + def number_of_surface_sites(self): + """ + Return the number of surface sites for a species. + eg. 2 for bidentate. + """ + return self.molecule[0].number_of_surface_sites() + + def is_electron(self): + """Return ``True`` if the species is an electron""" + + if len(self.molecule) == 0: + return False + else: + return self.molecule[0].is_electron() + + def is_proton(self): + """Return ``True`` if the species is a proton""" + + if len(self.molecule) == 0: + return False + else: + return self.molecule[0].is_proton() + def get_partition_function(self, T): """ Return the partition function for the species at the specified @@ -724,17 +774,17 @@ def copy(self, deep=False): return other - def get_augmented_inchi(self): + def get_augmented_inchi(self, backend='rdkit-first'): if self.aug_inchi is None: - self.aug_inchi = self.generate_aug_inchi() + self.aug_inchi = self.generate_aug_inchi(backend=backend) return self.aug_inchi - def generate_aug_inchi(self): + def generate_aug_inchi(self, backend='rdkit-first'): candidates = [] self.generate_resonance_structures() for mol in self.molecule: try: - cand = [mol.to_augmented_inchi(), mol] + cand = [mol.to_augmented_inchi(backend=backend), mol] except ValueError: pass # not all resonance structures can be parsed into InChI (e.g. if containing a hypervalance atom) else: diff --git a/molecule/thermo/model.pyx b/molecule/thermo/model.pyx index 4c5d5eb..5ac9658 100644 --- a/molecule/thermo/model.pyx +++ b/molecule/thermo/model.pyx @@ -163,13 +163,18 @@ cdef class HeatCapacityModel(RMGObject): Tdata = [300,400,500,600,800,1000,1500,2000] for T in Tdata: - if not (0.8 < self.get_heat_capacity(T) / other.get_heat_capacity(T) < 1.25): + # Do exact comparison in addition to relative in case both are zero (surface site) + if self.get_heat_capacity(T) != other.get_heat_capacity(T) and \ + not (0.8 < self.get_heat_capacity(T) / other.get_heat_capacity(T) < 1.25): return False - elif not (0.8 < self.get_enthalpy(T) / other.get_enthalpy(T) < 1.25): + elif self.get_enthalpy(T) != other.get_enthalpy(T) and \ + not (0.8 < self.get_enthalpy(T) / other.get_enthalpy(T) < 1.25): return False - elif not (0.8 < self.get_entropy(T) / other.get_entropy(T) < 1.25): + elif self.get_entropy(T) != other.get_entropy(T) and \ + not (0.8 < self.get_entropy(T) / other.get_entropy(T) < 1.25): return False - elif not (0.8 < self.get_free_energy(T) / other.get_free_energy(T) < 1.25): + elif self.get_free_energy(T) != other.get_free_energy(T) and \ + not (0.8 < self.get_free_energy(T) / other.get_free_energy(T) < 1.25): return False return True @@ -185,13 +190,18 @@ cdef class HeatCapacityModel(RMGObject): Tdata = [300,400,500,600,800,1000,1500,2000] for T in Tdata: - if not (0.95 < self.get_heat_capacity(T) / other.get_heat_capacity(T) < 1.05): + # Do exact comparison in addition to relative in case both are zero (surface site) + if self.get_heat_capacity(T) != other.get_heat_capacity(T) and \ + not (0.95 < self.get_heat_capacity(T) / other.get_heat_capacity(T) < 1.05): return False - elif not (0.95 < self.get_enthalpy(T) / other.get_enthalpy(T) < 1.05): + elif self.get_enthalpy(T) != other.get_enthalpy(T) and \ + not (0.95 < self.get_enthalpy(T) / other.get_enthalpy(T) < 1.05): return False - elif not (0.95 < self.get_entropy(T) / other.get_entropy(T) < 1.05): + elif self.get_entropy(T) != other.get_entropy(T) and \ + not (0.95 < self.get_entropy(T) / other.get_entropy(T) < 1.05): return False - elif not (0.95 < self.get_free_energy(T) / other.get_free_energy(T) < 1.05): + elif self.get_free_energy(T) != other.get_free_energy(T) and \ + not (0.95 < self.get_free_energy(T) / other.get_free_energy(T) < 1.05): return False return True diff --git a/molecule/thermo/thermoengine.py b/molecule/thermo/thermoengine.py index 50a1b23..9e05416 100644 --- a/molecule/thermo/thermoengine.py +++ b/molecule/thermo/thermoengine.py @@ -37,6 +37,7 @@ #from molecule.statmech import Conformer from molecule.thermo import Wilhoit, NASA, ThermoData from molecule.molecule import Molecule +from molecule.molecule.fragment import Fragment def process_thermo_data(spc, thermo0, thermo_class=NASA, solvent_name=''): @@ -69,7 +70,7 @@ def process_thermo_data(spc, thermo0, thermo_class=NASA, solvent_name=''): # 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) - wilhoit.comment += ' + Solvation correction with {} as solvent and solute estimated using {}'.format(solvent_name, solute_data.comment) + wilhoit.comment += f' + Solvation correction (H={solvation_correction.enthalpy/1e3:+.0f}kJ/mol;S={solvation_correction.entropy:+.0f}J/mol/K) with {solvent_name} as solvent and solute estimated using {solute_data.comment}' # Compute E0 by extrapolation to 0 K # if spc.conformer is None: @@ -157,7 +158,7 @@ def evaluator(spc, solvent_name=''): """ logging.debug("Evaluating spc %s ", spc) - if isinstance(spc.molecule[0], Molecule): + if not isinstance(spc.molecule[0], Fragment): spc.generate_resonance_structures() thermo = generate_thermo_data(spc, solvent_name=solvent_name) else: diff --git a/molecule/thermo/wilhoit.pyx b/molecule/thermo/wilhoit.pyx index 0cedbf5..e87cf20 100644 --- a/molecule/thermo/wilhoit.pyx +++ b/molecule/thermo/wilhoit.pyx @@ -273,8 +273,8 @@ cdef class Wilhoit(HeatCapacityModel): # What remains is to fit the polynomial coefficients (a0, a1, a2, a3) # This can be done directly - no iteration required - A = np.empty((Cpdata.shape[0],4), np.float64) - b = np.empty(Cpdata.shape[0], np.float64) + A = np.empty((Cpdata.shape[0],4), float) + b = np.empty(Cpdata.shape[0], float) for i in range(Cpdata.shape[0]): y = Tdata[i] / (Tdata[i] + B) for j in range(4): diff --git a/molecule/transportDataTest.py b/molecule/transportDataTest.py index e486bf0..66b7faa 100644 --- a/molecule/transportDataTest.py +++ b/molecule/transportDataTest.py @@ -28,7 +28,7 @@ ############################################################################### """ -This script contains unit test of the :mod: 'rmgpy.transport' module and :mod: 'rmgpy.data.transport' module +This script contains unit test of the :mod: 'molecule.transport' module and :mod: 'molecule.data.transport' module """ import unittest diff --git a/molecule/version.py b/molecule/version.py index 74415d5..1f2a642 100644 --- a/molecule/version.py +++ b/molecule/version.py @@ -33,4 +33,4 @@ This value can be accessed via `molecule.__version__`. """ -__version__ = '3.1.0' +__version__ = '3.2.0' diff --git a/molecule/yml.py b/molecule/yml.py index a4f8268..bb01ece 100644 --- a/molecule/yml.py +++ b/molecule/yml.py @@ -34,6 +34,7 @@ import os import yaml +import logging from molecule.chemkin import load_chemkin_file from molecule.species import Species @@ -125,6 +126,7 @@ def obj_to_dict(obj, spcs, names=None, label="solvent"): result_dict["henrylawconstant"]["type"] = "TemperatureDependentHenryLawConstant" result_dict["henrylawconstant"]["Ts"] = obj.henry_law_constant_data.Ts result_dict["henrylawconstant"]["kHs"] = obj.henry_law_constant_data.kHs + result_dict["comment"] = obj.thermo.comment elif isinstance(obj, NASA): result_dict["polys"] = [obj_to_dict(k, spcs) for k in obj.polynomials] result_dict["type"] = "NASA" @@ -140,12 +142,38 @@ def obj_to_dict(obj, spcs, names=None, label="solvent"): result_dict["type"] = "ElementaryReaction" result_dict["radicalchange"] = sum([get_radicals(x) for x in obj.products]) - \ sum([get_radicals(x) for x in obj.reactants]) + result_dict["electronchange"] = -sum([spc.molecule[0].get_net_charge() for spc in obj.products]) + sum([spc.molecule[0].get_net_charge() for spc in obj.reactants]) + result_dict["comment"] = obj.kinetics.comment elif isinstance(obj, Arrhenius): obj.change_t0(1.0) result_dict["type"] = "Arrhenius" result_dict["A"] = obj.A.value_si result_dict["Ea"] = obj.Ea.value_si result_dict["n"] = obj.n.value_si + elif isinstance(obj, ArrheniusChargeTransfer): + obj.change_t0(1.0) + obj.change_v0(0.0) + result_dict["type"] = "Arrheniusq" + result_dict["A"] = obj.A.value_si + result_dict["Ea"] = obj.Ea.value_si + result_dict["n"] = obj.n.value_si + result_dict["q"] = obj._alpha.value_si*obj._electrons.value_si + elif isinstance(obj, SurfaceChargeTransfer): + obj.change_v0(0.0) + result_dict["type"] = "Arrheniusq" + result_dict["A"] = obj.A.value_si + result_dict["Ea"] = obj.Ea.value_si + result_dict["n"] = obj.n.value_si + result_dict["q"] = obj._alpha.value_si*obj._electrons.value_si + elif isinstance(obj, Marcus): + result_dict["type"] = "Marcus" + result_dict["A"] = obj.A.value_si + result_dict["n"] = obj.n.value_si + result_dict["lmbd_i_coefs"] = obj.lmbd_i_coefs.value_si.tolist() + result_dict["lmbd_o"] = obj.lmbd_o.value_si + result_dict["wr"] = obj.wr.value_si + result_dict["wp"] = obj.wp.value_si + result_dict["beta"] = obj.beta.value_si elif isinstance(obj, StickingCoefficient): obj.change_t0(1.0) result_dict["type"] = "StickingCoefficient"