def template_matches(reactant, truncated_graph, ts_template): """ Determine if a transition state template matches a truncated graph. The truncated graph includes all the active bonds in the reaction and the nearest neighbours to those atoms e.g. for a Diels-Alder reaction H H \ / H-C----C-H where the dotted lines represent active bonds . . H . . H \. . / H - C C - H \ / C C Arguments: reactant (autode.complex.ReactantComplex): truncated_graph (nx.Graph): ts_template (autode.transition_states.templates.TStemplate): """ if reactant.charge != ts_template.charge or reactant.mult != ts_template.mult: return False if reactant.solvent != ts_template.solvent: return False if is_isomorphic(truncated_graph, ts_template.graph): logger.info('Found matching TS template') return True return False
def test_not_isomorphic(): h_c = Atom(atomic_symbol='H', x=0.0, y=0.0, z=1.0) h2_b = Species(name='template', charge=0, mult=1, atoms=[h_a, h_c]) mol_graphs.make_graph(species=h2_b, rel_tolerance=0.3) assert mol_graphs.is_isomorphic(h2.graph, h2_b.graph) is False
def test_not_isomorphic2(): c = Atom(atomic_symbol='C', x=0.0, y=0.0, z=0.7) ch = Species(name='ch', atoms=[h_a, c], charge=0, mult=2) mol_graphs.make_graph(ch) assert mol_graphs.is_isomorphic(h2.graph, ch.graph) is False
def _set_lowest_energy_conformer(self): """Set the species energy and atoms as those of the lowest energy conformer""" lowest_energy = None for conformer in self.conformers: if conformer.energy is None: continue # Conformers don't have a molecular graph, so make it make_graph(conformer) if not is_isomorphic(conformer.graph, self.graph, ignore_active_bonds=True): logger.warning('Conformer had a different graph. Ignoring') continue # If the conformer retains the same connectivity, up the the active # atoms in the species graph if lowest_energy is None: lowest_energy = conformer.energy if conformer.energy <= lowest_energy: self.energy = conformer.energy self.set_atoms(atoms=conformer.atoms) lowest_energy = conformer.energy return None
def test_reac_to_prods(): rearrang = BondRearrangement([(0, 4)], [(3, 4)]) prod_graph = mol_graphs.reac_graph_to_prod_graph(g, rearrang) expected_edges = [(0, 1), (1, 2), (2, 0), (0, 3), (0, 4)] expected_graph = nx.Graph() for edge in expected_edges: expected_graph.add_edge(*edge) assert mol_graphs.is_isomorphic(expected_graph, prod_graph)
def test_generate_rearranged_graph(): init_graph = nx.Graph() final_graph = nx.Graph() init_edges = [(0, 1), (1, 2), (2, 3), (4, 5), (5, 6)] final_edges = [(0, 1), (2, 3), (3, 4), (4, 5), (5, 6)] for edge in init_edges: init_graph.add_edge(*edge) for edge in final_edges: final_graph.add_edge(*edge) assert is_isomorphic(br.generate_rearranged_graph(init_graph, [(3, 4)], [(1, 2)]), final_graph)
def add_bond_rearrangment(bond_rearrangs, reactant, product, fbonds, bbonds): """For a possible bond rearrangement, sees if the products are made, and adds it to the bond rearrang list if it does Arguments: bond_rearrangs (list(autode.bond_rearrangements.BondRearrangement)): list of working bond rearrangments reactant (molecule object): reactant complex product (molecule object): product complex fbonds (list(tuple)): list of bonds to be made bbonds (list(tuple)): list of bonds to be broken Returns: (list(autode.bond_rearrangements.BondRearrangement)): """ # Check that the bond rearrangement doesn't exceed standard atom valances bbond_atoms = [atom for bbond in bbonds for atom in bbond] for fbond in fbonds: for atom in fbond: atom_label = reactant.atoms[atom].label if (reactant.graph.degree(atom) == get_maximal_valance(atom_label) and atom not in bbond_atoms): # If we are here then there is at least one atom that will # exceed it's maximal valance, therefore # we don't need to run isomorphism return bond_rearrangs rearranged_graph = generate_rearranged_graph(reactant.graph, fbonds=fbonds, bbonds=bbonds) if is_isomorphic(rearranged_graph, product.graph): ordered_fbonds = [] ordered_bbonds = [] for fbond in fbonds: if fbond[0] < fbond[1]: ordered_fbonds.append((fbond[0], fbond[1])) else: ordered_fbonds.append((fbond[1], fbond[0])) for bbond in bbonds: if bbond[0] < bbond[1]: ordered_bbonds.append((bbond[0], bbond[1])) else: ordered_bbonds.append((bbond[1], bbond[0])) ordered_fbonds.sort() ordered_bbonds.sort() bond_rearrangs.append( BondRearrangement(forming_bonds=ordered_fbonds, breaking_bonds=ordered_bbonds)) return bond_rearrangs
def products_made(self): logger.info('Checking that somewhere on the surface product(s) are made') for i in range(self.n_points): make_graph(self.species[i]) if is_isomorphic(graph1=self.species[i].graph, graph2=self.product_graph): logger.info(f'Products made at point {i} in the 1D surface') return True return False
def test_isomorphic_no_active(): os.chdir(os.path.join(here, 'data')) ts_syn = Conformer(name='syn_ts', charge=-1, mult=0, atoms=xyz_file_to_atoms('E2_ts_syn.xyz')) mol_graphs.make_graph(ts_syn) mol_graphs.set_active_mol_graph(ts_syn, active_bonds=[(8, 5), (0, 5), (1, 2)]) ts_anti = Conformer(name='anti_ts', charge=-1, mult=0, atoms=xyz_file_to_atoms('E2_ts.xyz')) mol_graphs.make_graph(ts_anti) assert mol_graphs.is_isomorphic(ts_syn.graph, ts_anti.graph, ignore_active_bonds=True) os.chdir(here)
def test_core_strip(): bond_rearr = BondRearrangement() bond_rearr.active_atoms = [0] stripped = get_truncated_complex(methane, bond_rearr) # Should not strip any atoms if the carbon is designated as active assert stripped.n_atoms == 5 stripped = get_truncated_complex(ethene, bond_rearr) assert stripped.n_atoms == 6 bond_rearr.active_atoms = [1] # Propene should strip to ethene if the terminal C=C is the active atom stripped = get_truncated_complex(propene, bond_rearr) assert stripped.n_atoms == 6 assert is_isomorphic(stripped.graph, ethene.graph) # But-1-ene should strip to ethene if the terminal C=C is the active atom stripped = get_truncated_complex(but1ene, bond_rearr) assert stripped.n_atoms == 6 assert is_isomorphic(stripped.graph, ethene.graph) # Benzene shouldn't be truncated at all stripped = get_truncated_complex(benzene, bond_rearr) assert stripped.n_atoms == 12 bond_rearr.active_atoms = [0] # Ethanol with the terminal C as the active atom should not replace the OH # with a H stripped = get_truncated_complex(ethanol, bond_rearr) assert stripped.n_atoms == 9 # Ether with the terminal C as the active atom should replace the OMe with # OH stripped = get_truncated_complex(methlyethylether, bond_rearr) assert stripped.n_atoms == 9 assert is_isomorphic(stripped.graph, ethanol.graph)
def products_made(self): """Check that somewhere on the surface the molecular graph is isomorphic to the product""" logger.info('Checking product(s) are made somewhere on the surface') for i in range(self.n_points_r1): for j in range(self.n_points_r2): make_graph(self.species[i, j]) if is_isomorphic(graph1=self.species[i, j].graph, graph2=self.product_graph): logger.info(f'Products made at ({i}, {j})') return True return False
def test_ts_template(): h_c = Atom(atomic_symbol='H', x=0.0, y=0.0, z=1.4) ts_template = Species(name='template', charge=0, mult=1, atoms=[h_a, h_b, h_c]) mol_graphs.make_graph(species=ts_template, allow_invalid_valancies=True) ts_template.graph.edges[0, 1]['active'] = True ts = Species(name='template', charge=0, mult=1, atoms=[h_a, h_b, h_c]) mol_graphs.make_graph(species=ts, allow_invalid_valancies=True) ts.graph.edges[1, 2]['active'] = True mapping = mol_graphs.get_mapping_ts_template(ts.graph, ts_template.graph) assert mapping is not None assert type(mapping) == dict assert mol_graphs.is_isomorphic(ts.graph, ts_template.graph, ignore_active_bonds=True)
def products_made(self, product): """Check whether the products are made on the surface Arguments: product (autode.species.Species): Returns: (bool): """ if product is None or product.graph is None: logger.warning('Cannot check if products are made') return False for i, point in enumerate(self): if mol_graphs.is_isomorphic(graph1=point.species.graph, graph2=product.graph): logger.info(f'Products made at point {i}') return True return False
def test_timeout(): # Generate a large-ish graph graph = nx.Graph() for i in range(10000): graph.add_node(i) for _ in range(5000): (i, j) = np.random.randint(0, 1000, size=2) if (i, j) not in graph.edges: graph.add_edge(i, j) node_perm = np.random.permutation(list(graph.nodes)) mapping = {u: v for (u, v) in zip(graph.nodes, node_perm)} isomorphic_graph = nx.relabel_nodes(graph, mapping=mapping, copy=True) # With a short timeout this should return False - not sure this is the # optimal behavior assert not mol_graphs.is_isomorphic(graph, isomorphic_graph, timeout=1)
def test_isomorphic_graphs(): h2_alt = Species(name='H2', atoms=[h_b, h_a], charge=0, mult=1) mol_graphs.make_graph(h2_alt) assert mol_graphs.is_isomorphic(h2.graph, h2_alt.graph) is True
def get_sum_energy_mep(saddle_point_r1r2, pes_2d): """ Calculate the sum of the minimum energy path that traverses reactants (r) to products (p) via the saddle point (s):: / p / s r2 / /r ------------ r1 Arguments: saddle_point_r1r2 (tuple(float)): pes_2d (autode.pes_2d.PES2d): Returns: (float): Path energy (Ha) """ logger.info('Finding the total energy along the minimum energy pathway') reactant_point = (0, 0) product_point, product_energy = None, 9999 # The saddle point indexes are those that are closest tp the saddle points # r1 and r2 distances saddle_point = (np.argmin(np.abs(pes_2d.r1s - saddle_point_r1r2[0])), np.argmin(np.abs(pes_2d.r2s - saddle_point_r1r2[1]))) # Generate a grid graph (i.e. all nodes are corrected energy_graph = nx.grid_2d_graph(pes_2d.n_points_r1, pes_2d.n_points_r2) min_energy = min([species.energy for species in pes_2d.species.flatten()]) # For energy point on the 2D surface for i in range(pes_2d.n_points_r1): for j in range(pes_2d.n_points_r2): point_rel_energy = pes_2d.species[i, j].energy - min_energy # Populate the relative energy of each node in the graph energy_graph.nodes[i, j]['energy'] = point_rel_energy # Find the point where products are made if is_isomorphic(graph1=pes_2d.species[i, j].graph, graph2=pes_2d.product_graph): # If products have not yet found, or they have and the energy # are lower but are still isomorphic if product_point is None or point_rel_energy < product_energy: product_point = (i, j) product_energy = point_rel_energy logger.info(f'Reactants at r1={pes_2d.r1s[0]:.4f} , ' f'r2={pes_2d.r2s[0]:.4f} Å and ' f'products r1={pes_2d.rs[product_point][0]:.4f}, ' f'r2={pes_2d.rs[product_point][1]:.4f} Å') def energy_diff(curr_node, final_node, d): """Energy difference between the twp points on the graph. d is required to satisfy nx. Must only increase in energy to a saddle point so take the magnitude to prevent traversing s mistakenly""" return (np.abs(energy_graph.nodes[final_node]['energy'] - energy_graph.nodes[curr_node]['energy'])) # Calculate the energy along the MEP up to the saddle point from reactants # and products path_energy = 0.0 for point in (reactant_point, product_point): path_energy += nx.dijkstra_path_length(energy_graph, source=point, target=saddle_point, weight=energy_diff) logger.info(f'Path energy to {saddle_point} is {path_energy:.4f} Hd') return path_energy
def get_bond_rearrangs(reactant, product, name, save=True): """For a reactant and product (complex) find the set of breaking and forming bonds that will turn reactants into products. This works by determining the types of bonds that have been made/broken (i.e CH) and then only considering rearrangements involving those bonds. Arguments: reactant (autode.complex.ReactantComplex): product (autode.complex.ProductComplex): name (str): Keyword Arguments: save (bool): Save bond rearrangements to a file for fast reloading Returns: (list(autode.bond_rearrangements.BondRearrangement)): """ logger.info(f'Finding the possible forming and breaking bonds for {name}') if os.path.exists(f'{name}_bond_rearrangs.txt'): return get_bond_rearrangs_from_file(f'{name}_bond_rearrangs.txt') if is_isomorphic(reactant.graph, product.graph) and product.n_atoms > 3: logger.error('Reactant (complex) is isomorphic to product (complex). ' 'Bond rearrangement cannot be determined unless the ' 'substrates are limited in size') return None possible_brs = [] reac_bond_dict = get_bond_type_list(reactant.graph) prod_bond_dict = get_bond_type_list(product.graph) # list of bonds where this type of bond (e.g C-H) has less bonds in # products than reactants all_possible_bbonds = [] # list of bonds that can be formed of this bond type. This is only used # if there is only one type of bbond, so can be overwritten for each new # type of bbond bbond_atom_type_fbonds = None # list of bonds where this type of bond (e.g C-H) has more bonds in # products than reactants all_possible_fbonds = [] # list of bonds that can be broken of this bond type. This is only used # if there is only one type of fbond, so can be overwritten for each new # type of fbond fbond_atom_type_bbonds = None # list of bonds where this type of bond (e.g C-H) has the same number of # bonds in products and reactants possible_bbond_and_fbonds = [] for reac_key, reac_bonds in reac_bond_dict.items(): prod_bonds = prod_bond_dict[reac_key] possible_fbonds = get_fbonds(reactant.graph, reac_key) if len(prod_bonds) < len(reac_bonds): all_possible_bbonds.append(reac_bonds) bbond_atom_type_fbonds = possible_fbonds elif len(prod_bonds) > len(reac_bonds): all_possible_fbonds.append(possible_fbonds) fbond_atom_type_bbonds = reac_bonds else: if len(reac_bonds) != 0: possible_bbond_and_fbonds.append([reac_bonds, possible_fbonds]) # The change in the number of bonds is > 0 as in the reaction # initialisation reacs/prods are swapped if this is < 0 delta_n_bonds = (reactant.graph.number_of_edges() - product.graph.number_of_edges()) if delta_n_bonds == 0: funcs = [get_fbonds_bbonds_1b1f, get_fbonds_bbonds_2b2f] elif delta_n_bonds == 1: funcs = [get_fbonds_bbonds_1b, get_fbonds_bbonds_2b1f] elif delta_n_bonds == 2: funcs = [get_fbonds_bbonds_2b] else: logger.error(f'Cannot treat a change in bonds ' f'reactant <- product of {delta_n_bonds}') return None for func in funcs: possible_brs = func(reactant, product, possible_brs, all_possible_bbonds, all_possible_fbonds, possible_bbond_and_fbonds, bbond_atom_type_fbonds, fbond_atom_type_bbonds) if len(possible_brs) > 0: logger.info(f'Found a molecular graph rearrangement to products ' f'with {func.__name__}') # This function will return with the first bond rearrangement # that leads to products n_bond_rearrangs = len(possible_brs) if n_bond_rearrangs > 1: logger.info(f'Multiple *{n_bond_rearrangs}* possible bond ' f'breaking/makings are possible') possible_brs = strip_equiv_bond_rearrs(possible_brs, reactant) prune_small_ring_rearrs(possible_brs, reactant) if save: save_bond_rearrangs_to_file(possible_brs, filename=f'{name}_BRs.txt') logger.info(f'Found *{len(possible_brs)}* bond ' f'rearrangement(s) that lead to products') return possible_brs return None