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 init_smiles(molecule, smiles): """ Initialise a molecule from a SMILES string Arguments: molecule (autode.molecule.Molecule): smiles (str): SMILES string """ # Assume that the RDKit conformer generation algorithm is not okay for # metals molecule.rdkit_conf_gen_is_fine = False parser = parse_smiles(smiles) molecule.charge = parser.charge molecule.mult = calc_multiplicity( molecule=molecule, n_radical_electrons=parser.n_radical_electrons) molecule.set_atoms(atoms=parser.atoms) make_graph(molecule, bond_list=parser.bonds) for stereocentre in parser.stereocentres: molecule.graph.nodes[stereocentre]['stereo'] = True for bond_index in parser.bond_order_dict.keys(): bond = parser.bonds[bond_index] molecule.graph.edges[bond]['pi'] = True molecule.set_atoms(atoms=get_simanl_atoms(molecule)) check_bonds(molecule, bonds=parser.bonds) return None
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 test_set_pi_bonds(): ethene = Species(name='ethene', charge=0, mult=1, atoms=[ Atom('C', -2.20421, 0.40461, 0.00000), Atom('C', -0.87115, 0.38845, 0.00000), Atom('H', -2.76098, -0.22576, 0.68554), Atom('H', -2.74554, 1.04829, -0.68554), Atom('H', -0.32982, -0.25523, 0.68554), Atom('H', -0.31437, 1.01882, -0.68554) ]) mol_graphs.make_graph(ethene) assert ethene.graph.edges[0, 1]['pi'] is True assert ethene.graph.edges[1, 0]['pi'] is True assert ethene.graph.edges[0, 2]['pi'] is False acetylene = Species(name='acetylene', charge=0, mult=1, atoms=[ Atom('C', -2.14031, 0.40384, 0.00000), Atom('C', -0.93505, 0.38923, 0.00000), Atom('H', -3.19861, 0.41666, 0.00000), Atom('H', 0.12326, 0.37640, 0.00000) ]) mol_graphs.make_graph(acetylene) assert acetylene.graph.edges[0, 1]['pi'] is True assert acetylene.graph.edges[0, 2]['pi'] is False
def get_optimised_species(calc, method, direction, atoms): """Get the species that is optimised from an initial set of atoms""" species = Molecule(name=f'{calc.name}_{direction}', atoms=atoms, charge=calc.molecule.charge, mult=calc.molecule.mult) # Note that for the surface to be the same the keywords.opt and keywords.hess need to match in the level of theory calc = Calculation(name=f'{calc.name}_{direction}', molecule=species, method=method, keywords=method.keywords.opt, n_cores=Config.n_cores) calc.run() try: species.set_atoms(atoms=calc.get_final_atoms()) species.energy = calc.get_energy() make_graph(species) except AtomsNotFound: logger.error(f'{direction} displacement calculation failed') return species
def imag_mode_generates_other_bonds(ts, f_species, b_species, bond_rearrangement): """Determine if the forward or backwards displaced molecule break or make bonds that aren't in all the active bonds bond_rearrangement.all. Will be fairly conservative here""" for species in (ts, f_species, b_species): make_graph(species, rel_tolerance=0.3) for product in (f_species, b_species): new_bonds_in_product = set([ bond for bond in product.graph.edges if bond not in ts.graph.edges ]) # If there are new bonds in the forward displaced species that are not # part of the bond rearrangement if any(bond not in bond_rearrangement.all for bond in new_bonds_in_product): logger.warning(f'New bonds in product: {new_bonds_in_product}') logger.warning(f'Bond rearrangement: {bond_rearrangement.all}') return True logger.info('Imaginary mode does not generate any other unwanted bonds') return False
def optimise(self, method=None, reset_graph=False, calc=None): """ Optimise the geometry using a method Arguments: method (autode.wrappers.base.ElectronicStructureMethod): Keyword Arguments: reset_graph (bool): Reset the molecular graph calc (autode.calculation.Calculation): Different e.g. constrained optimisation calculation """ logger.info(f'Running optimisation of {self.name}') if calc is None: assert method is not None calc = Calculation(name=f'{self.name}_opt', molecule=self, method=method, keywords=method.keywords.opt, n_cores=Config.n_cores) else: assert isinstance(calc, Calculation) calc.run() self.energy = calc.get_energy() self.set_atoms(atoms=calc.get_final_atoms()) self.print_xyz_file(filename=f'{self.name}_optimised_{method.name}.xyz') if reset_graph: make_graph(self) return None
def test_set_lowest_energy_conformer(): from autode.mol_graphs import make_graph hb = Atom('H', z=0.7) hydrogen = Species(name='H2', atoms=[h1, hb], charge=0, mult=1) make_graph(hydrogen) hydrogen_wo_e = Species(name='H2', atoms=[h1, hb], charge=0, mult=1) hydrogen_with_e = Species(name='H2', atoms=[h1, hb], charge=0, mult=1) hydrogen_with_e.energy = -1 hydrogen.conformers = [hydrogen_wo_e, hydrogen_with_e] hydrogen._set_lowest_energy_conformer() # Conformers without energy should be skipped assert hydrogen.energy == -1 # Conformers with a different molecular graph should be skipped h_atom = Species(name='H', atoms=[Atom('H')], charge=0, mult=1) h_atom.energy = -2 hydrogen.conformers = [hydrogen_with_e, h_atom] assert hydrogen.energy == -1
def test_3b(): reac = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('H', 0.6, 0, 0), Atom('H', 1.2, 0, 0), Atom('H', 1.8, 0, 0)]) make_graph(reac, allow_invalid_valancies=True) prod = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('H', 10, 0, 0), Atom('H', 20, 0, 0), Atom('H', 30, 0, 0)]) # Reactants to products must break three bonds but this is not yet supported in any form assert br.get_bond_rearrangs(ReactantComplex(reac), ProductComplex(prod), name='3b_test') is None
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_edge_cases(): h_c = Atom(atomic_symbol='H', x=0.0, y=0.0, z=1.6) # For H3 with a slightly longer bond on one side there should only be 1 'bond' h3 = Species(name='H2', atoms=[h_a, h_b, h_c], charge=0, mult=1) mol_graphs.make_graph(h3) assert h3.graph.number_of_edges() == 1 assert h3.graph.number_of_nodes() == 3
def test_2b(): reac = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('H', 0.6, 0, 0), Atom('H', 1.2, 0, 0)]) make_graph(reac, allow_invalid_valancies=True) prod = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('H', 10, 0, 0), Atom('H', 20, 0, 0)]) # Reactants to products must break two bonds assert len(br.get_bond_rearrangs(ReactantComplex(reac), ProductComplex(prod), name='2b_test')) == 1 os.remove('2b_test_bond_rearrangs.txt') assert br.get_fbonds_bbonds_2b(reac, prod, [], [[(0, 1), (1, 2)]], [], [], [(0, 2)], []) == [br.BondRearrangement(breaking_bonds=[(0, 1), (1, 2)])]
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_truncated_active_graph(): h_c = Atom(atomic_symbol='H', x=0.0, y=0.0, z=1.4) h_d = Atom(atomic_symbol='H', x=0.0, y=0.0, z=2.1) ts = Species(name='template', charge=0, mult=1, atoms=[h_a, h_b, h_c, h_d]) mol_graphs.make_graph(species=ts, allow_invalid_valancies=True) # H--active--H--H--H should truncate by keeping only the nearest neighbours to the first two atoms truncated_graph = mol_graphs.get_truncated_active_mol_graph(ts.graph, active_bonds=[(0, 1)]) assert truncated_graph.number_of_nodes() == 3 assert truncated_graph.number_of_edges() == 2
def init_organic_smiles(molecule, smiles): """ Initialise a molecule from a SMILES string, set the charge, multiplicity ( if it's not already specified) and the 3D geometry using RDKit Arguments: molecule (autode.molecule.Molecule): smiles (str): SMILES string """ try: molecule.rdkit_mol_obj = Chem.MolFromSmiles(smiles) if molecule.rdkit_mol_obj is None: logger.warning('RDKit failed to initialise a molecule') return init_smiles(molecule, smiles) molecule.rdkit_mol_obj = Chem.AddHs(molecule.rdkit_mol_obj) except RuntimeError: raise RDKitFailed molecule.charge = Chem.GetFormalCharge(molecule.rdkit_mol_obj) molecule.mult = calc_multiplicity(molecule, NumRadicalElectrons(molecule.rdkit_mol_obj)) bonds = [(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()) for bond in molecule.rdkit_mol_obj.GetBonds()] # Generate a single 3D structure using RDKit's ETKDG conformer generation # algorithm method = AllChem.ETKDGv2() method.randomSeed = 0xf00d AllChem.EmbedMultipleConfs(molecule.rdkit_mol_obj, numConfs=1, params=method) molecule.atoms = atoms_from_rdkit_mol(molecule.rdkit_mol_obj, conf_id=0) make_graph(molecule, bond_list=bonds) # Revert back to RR if RDKit fails to return a sensible geometry if not are_coords_reasonable(coords=molecule.coordinates): molecule.rdkit_conf_gen_is_fine = False molecule.atoms = get_simanl_atoms(molecule, save_xyz=False) for atom, _ in Chem.FindMolChiralCenters(molecule.rdkit_mol_obj): molecule.graph.nodes[atom]['stereo'] = True for bond in molecule.rdkit_mol_obj.GetBonds(): if bond.GetBondType() != Chem.rdchem.BondType.SINGLE: molecule.graph.edges[bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()]['pi'] = True if bond.GetStereo() != Chem.rdchem.BondStereo.STEREONONE: molecule.graph.nodes[bond.GetBeginAtomIdx()]['stereo'] = True molecule.graph.nodes[bond.GetEndAtomIdx()]['stereo'] = True check_bonds(molecule, bonds=molecule.rdkit_mol_obj.GetBonds()) return None
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_mapping(): h_c = Atom(atomic_symbol='H', x=0.0, y=0.0, z=1.4) h3_a = Species(name='template', charge=0, mult=1, atoms=[h_a, h_b, h_c]) mol_graphs.make_graph(species=h3_a, allow_invalid_valancies=True) h3_b = Species(name='template', charge=0, mult=1, atoms=[h_a, h_b, h_c]) mol_graphs.make_graph(species=h3_b, allow_invalid_valancies=True) # Isomorphic (identical) graphs should have at least one mapping between them mapping = mol_graphs.get_mapping(h3_b.graph, h3_a.graph) assert mapping is not None assert type(mapping) == dict
def _init_graph(self): """Set the molecular graph for this TS object from the reactant""" if self.reactant is not None: logger.warning(f'Setting the graph of {self.name} from reactants') self.graph = self.reactant.graph.copy() elif self.atoms is not None: logger.warning(f'Setting the graph of {self.name} from atoms') make_graph(self) else: logger.warning('Have no TS graph') return None
def test_subgraph_isomorphism(): h_c = Atom(atomic_symbol='H', x=0.0, y=0.0, z=1.4) h_d = Atom(atomic_symbol='H', x=0.0, y=0.0, z=2.1) h4 = Species(name='H4', atoms=[h_a, h_b, h_c, h_d], charge=0, mult=1) mol_graphs.make_graph(h4) assert mol_graphs.is_subgraph_isomorphic(larger_graph=h4.graph, smaller_graph=h2.graph) is True # H3 in a triangular arrangement should not be sub-graph isomorphic to linear H4 h_e = Atom(atomic_symbol='H', x=0.3, y=0.0, z=0.3) h3 = Species(name='H_H', charge=0, mult=1, atoms=[h_a, h_b, h_e]) mol_graphs.make_graph(h3, allow_invalid_valancies=True) assert mol_graphs.is_subgraph_isomorphic(larger_graph=h4.graph, smaller_graph=h3.graph) is False
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 check_bonds(molecule, bonds): """ Ensure the SMILES string and the 3D structure have the same bonds, but don't override Arguments: molecule (autode.molecule.Molecule): bonds (list): """ check_molecule = deepcopy(molecule) make_graph(check_molecule) if len(bonds) != check_molecule.graph.number_of_edges(): logger.error('Bonds and graph do no match') return None
def __init__(self, name='molecule', smiles=None, atoms=None, solvent_name=None, charge=0, mult=1): """ Molecule class Keyword Arguments: name (str): Name of the molecule or a .xyz filename smiles (str): Standard SMILES string. e.g. generated by Chemdraw atoms (list(autode.atoms.Atom)): List of atoms in the species solvent_name (str): Solvent that the molecule is immersed in charge (int): Charge on the molecule mult (int): Spin multiplicity on the molecule """ logger.info(f'Generating a Molecule object for {name}') super().__init__(name, atoms, charge, mult, solvent_name) if name.endswith('.xyz'): self._init_xyz_file(xyz_filename=name) self.smiles = smiles self.rdkit_mol_obj = None self.rdkit_conf_gen_is_fine = True self.conformers = None if smiles is not None: self._init_smiles(smiles) elif atoms is not None: make_graph(self) # If the name is unassigned use a more interpretable chemical formula if name == 'molecule' and self.atoms is not None: self.name = self.formula()
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 append(self, point) -> None: """ Append a point to the path and optimise it Arguments: point (PathPoint): Point on a path Raises: (autode.exceptions.CalculationException): """ super().append(point) idx = len(self) - 1 calc = ade.Calculation(name=f'path_opt{idx}', molecule=self[idx].species, method=self.method, keywords=self.method.keywords.low_opt, n_cores=ade.Config.n_cores, distance_constraints=self[idx].constraints) calc.run() # Set the required properties from the calculation self[idx].species.atoms = calc.get_final_atoms() make_graph(self[idx].species) self[idx].energy = calc.get_energy() if self.method.name == 'xtb' or self.method.name == 'mopac': # XTB prints gradients including the constraints, which are ~0 # the gradient here is just the derivative of the electronic energy # so rerun a gradient calculation, which should be very fast # while MOPAC doesn't print gradients for a constrained opt calc = ade.Calculation(name=f'path_grad{idx}', molecule=self[idx].species, method=self.method, keywords=self.method.keywords.grad, n_cores=ade.Config.n_cores) calc.run() self[idx].grad = calc.get_gradients() calc.clean_up(force=True, everything=True) else: self[idx].grad = calc.get_gradients() return None
def test_remove_bonds(): b3h6 = Species(name='diborane', charge=0, mult=1, atoms=[Atom('B', -1.97106, 0.36170, -0.23984), Atom('H', -0.91975, -0.06081, 0.43901), Atom('H', -2.14001, -0.24547, -1.26544), Atom('H', -2.99029, 0.31275, 0.39878), Atom('B', -0.49819, 1.17500, 0.23984), Atom('H', 0.52102, 1.22392, -0.39880), Atom('H', -0.32919, 1.78217, 1.26543), Atom('H', -1.54951, 1.59751, -0.43898)]) mol_graphs.make_graph(species=b3h6) assert b3h6.graph.number_of_edges() == 6 assert b3h6.graph.number_of_nodes() == 8 # Boron atoms should be 3 fold valent assert len(list(b3h6.graph.neighbors(0))) == 3 assert len(list(b3h6.graph.neighbors(4))) == 3
def test_species_isomorphism(): h2.graph = None h2_copy = Species(name='H2', atoms=[h_a, h_b], charge=0, mult=1) h2_copy.graph = None with pytest.raises(NoMolecularGraph): assert mol_graphs.species_are_isomorphic(h2, h2_copy) # With molecular graphs the species should be isomorphic mol_graphs.make_graph(h2) mol_graphs.make_graph(h2_copy) assert mol_graphs.species_are_isomorphic(h2, h2_copy) # Shift one of the atoms far away and remake the graph h2_copy.atoms[1].translate(vec=np.array([10, 0, 0])) mol_graphs.make_graph(h2_copy) assert mol_graphs.species_are_isomorphic(h2, h2_copy) is False # Generating a pair of conformers that are isomporhpic should return that # the species are again isomorphic h2.conformers = [ Conformer(name='h2_conf', atoms=[h_a, h_b], charge=0, mult=1) ] h2_copy.conformers = [ Conformer(name='h2_conf', atoms=[h_a, h_b], charge=0, mult=1) ] assert mol_graphs.species_are_isomorphic(h2, h2_copy)
def test_prune_small_rings3(): # Square H4 "molecule" h4 = Molecule(atoms=[ Atom('H'), Atom('H', x=0.5), Atom('H', y=0.5), Atom('H', x=0.5, y=0.5) ]) make_graph(h4, allow_invalid_valancies=True) # Some unphysical bond rearrangements three_mem = BondRearrangement(forming_bonds=[(0, 3)], breaking_bonds=[(1, 2)]) four_mem = BondRearrangement(forming_bonds=[(0, 1)], breaking_bonds=[(1, 2)]) bond_rearrs = [three_mem, four_mem] ade.Config.skip_small_ring_tss = True br.prune_small_ring_rearrs(bond_rearrs, h4) # Should not prune if there are different ring sizes assert len(bond_rearrs) == 2
def test_2b1f(): reac = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('C', 0.6, 0, 0), Atom('O', 1.2, 0, 0)]) make_graph(reac, allow_invalid_valancies=True) prod = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('C', 10, 0, 0), Atom('O', 0.6, 0, 0)]) assert br.get_fbonds_bbonds_2b1f(reac, prod, [], [[(0, 1)], [(1, 2)]], [[(0, 2)]], [], [], []) == [br.BondRearrangement(forming_bonds=[(0, 2)], breaking_bonds=[(0, 1), (1, 2)])] reac = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('C', 0.6, 0, 0), Atom('H', 1.2, 0, 0)]) make_graph(reac, allow_invalid_valancies=True) prod = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('C', 10, 0, 0), Atom('H', 0.6, 0, 0)]) assert br.get_fbonds_bbonds_2b1f(reac, prod, [], [[(0, 1), (1, 2)]], [[(0, 2)]], [], [], []) == [br.BondRearrangement(forming_bonds=[(0, 2)], breaking_bonds=[(0, 1), (1, 2)])] reac = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('H', 0.6, 0, 0), Atom('H', 1.2, 0, 0)]) make_graph(reac, allow_invalid_valancies=True) prod = Molecule(atoms=[Atom('H', 0, 0, 0), Atom('H', 0.6, 0, 0), Atom('H', 10, 0, 0)]) assert br.get_fbonds_bbonds_2b1f(reac, prod, [], [[(0, 1), (1, 2)]], [], [], [(0, 2)], []) == [br.BondRearrangement(forming_bonds=[(0, 2)], breaking_bonds=[(0, 1), (1, 2)])]
from autode.pes.pes_2d import PES2d from autode.pes.pes import get_closest_species from autode.species.complex import ReactantComplex, ProductComplex from autode.atoms import Atom from autode.species.species import Species from autode.mol_graphs import make_graph from autode.exceptions import NoClosestSpecies from autode.pes.pes import FormingBond, BreakingBond from autode.reactions.reaction import Reaction from autode.species.molecule import Reactant, Product import pytest import numpy as np mol = Species(name='H2', charge=0, mult=1, atoms=[Atom('H'), Atom('H', z=1.0)]) make_graph(mol) reactant = ReactantComplex(mol, mol) product = ProductComplex(mol, mol) pes = PES2d(reactant=reactant, product=reactant, r1s=np.linspace(1, 3, 3), r1_idxs=(0, 1), r2s=np.linspace(1, 0, 2), r2_idxs=(2, 3)) def test_2d_pes_class(): assert reactant.n_atoms == 4 assert pes.rs.shape == (3, 2)
def test_check_rearrangement(): # Linear H3 -> Trigonal H3 make_graph(species=trig_h3, allow_invalid_valancies=True) reac = reaction.Reaction(lin_h3, trig_h3)