def test_are_coords_reasonable(): good_coords = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) assert geom.are_coords_reasonable(coords=good_coords) is True bad_coords1 = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.5]]) assert geom.are_coords_reasonable(coords=bad_coords1) is False bad_coords2 = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 2.0, 0.0], [2.0, 0.0, 0.0]]) assert geom.are_coords_reasonable(coords=bad_coords2) is False
def test_ts_conformer(tmpdir): os.chdir(tmpdir) ch3cl = Reactant(charge=0, mult=1, atoms=[ Atom('Cl', 1.63664, 0.02010, -0.05829), Atom('C', -0.14524, -0.00136, 0.00498), Atom('H', -0.52169, -0.54637, -0.86809), Atom('H', -0.45804, -0.50420, 0.92747), Atom('H', -0.51166, 1.03181, -0.00597) ]) f = Reactant(charge=-1, mult=1, atoms=[Atom('F', 4.0, 0.0, 0.0)]) ch3f = Product(charge=0, mult=1, atoms=[ Atom('C', -0.05250, 0.00047, -0.00636), Atom('F', 1.31229, -0.01702, 0.16350), Atom('H', -0.54993, -0.04452, 0.97526), Atom('H', -0.34815, 0.92748, -0.52199), Atom('H', -0.36172, -0.86651, -0.61030) ]) cl = Reactant(charge=-1, mult=1, atoms=[Atom('Cl', 4.0, 0.0, 0.0)]) f_ch3cl_tsguess = TSguess(reactant=ReactantComplex(f, ch3cl), product=ProductComplex(ch3f, cl), atoms=[ Atom('F', -2.66092, -0.01426, 0.09700), Atom('Cl', 1.46795, 0.05788, -0.06166), Atom('C', -0.66317, -0.01826, 0.02488), Atom('H', -0.78315, -0.58679, -0.88975), Atom('H', -0.70611, -0.54149, 0.97313), Atom('H', -0.80305, 1.05409, 0.00503) ]) f_ch3cl_tsguess.bond_rearrangement = BondRearrangement(breaking_bonds=[ (2, 1) ], forming_bonds=[(0, 2)]) f_ch3cl_ts = TransitionState(ts_guess=f_ch3cl_tsguess) atoms = conf_gen.get_simanl_atoms( species=f_ch3cl_ts, dist_consts=get_distance_constraints(f_ch3cl_ts)) regen = Molecule(name='regenerated_ts', charge=-1, mult=1, atoms=atoms) # regen.print_xyz_file() # Ensure the making/breaking bonds retain their length regen_coords = regen.get_coordinates() assert are_coords_reasonable(regen_coords) is True assert 1.9 < np.linalg.norm(regen_coords[0] - regen_coords[2]) < 2.1 assert 2.0 < np.linalg.norm(regen_coords[1] - regen_coords[2]) < 2.2 os.chdir(here)
def test_metal_eta_complex(tmpdir): os.chdir(tmpdir) # eta-6 benzene Fe2+ complex used in the molassembler paper m = Molecule(smiles='[C@@H]12[C@H]3[C@H]4[C@H]5[C@H]6[C@@H]1[Fe]265437N' '(C8=CC=CC=C8)C=CC=[N+]7C9=CC=CC=C9') m.print_xyz_file() assert are_coords_reasonable(coords=m.get_coordinates()) os.chdir(here)
def test_siman_conf_gen(tmpdir): os.chdir(tmpdir) rh_complex = Molecule(name='[RhH(CO)3(ethene)]', smiles='O=C=[Rh]1(=C=O)(CC1)([H])=C=O') assert are_coords_reasonable(coords=rh_complex.get_coordinates()) assert rh_complex.n_atoms == 14 assert 12 < rh_complex.graph.number_of_edges() < 15 # What is a bond even os.chdir(here)
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_nci_complex(): water = Molecule(name='water', smiles='O') f = Molecule(name='formaldehyde', smiles='C=O') nci_complex = NCIComplex(f, water) assert nci_complex.n_atoms == 7 # Set so the number of conformers doesn't explode Config.num_complex_sphere_points = 6 Config.num_complex_random_rotations = 4 nci_complex._generate_conformers() assert len(nci_complex.conformers) == 24 for conformer in nci_complex.conformers: # conformer.print_xyz_file() assert geom.are_coords_reasonable(coords=conformer.get_coordinates())
def _reasonable_components_with_energy(self): """Generator for components of a reaction that have sensible geometries and also energies""" reacs_prods = self.reacs + self.prods for mol in reacs_prods + [self.ts, self.reactant, self.product]: if mol is None: logger.warning('mol=None') continue if mol.energy is None: logger.warning(f'{mol.name} current energy was None') continue if not are_coords_reasonable(mol.coordinates): logger.warning(f'{mol.name} coordinates not reasonable') continue yield mol return None
def test_salt(): salt = Molecule(name='salt', smiles='[Li][Br]') assert salt.n_atoms == 2 assert are_coords_reasonable(coords=salt.get_coordinates()) os.remove('salt_conf0_siman.xyz')
def test_salt(): salt = Molecule(name='salt', smiles='[Li][Br]') assert salt.n_atoms == 2 assert are_coords_reasonable(coords=salt.coordinates)
def get_simanl_atoms(species, dist_consts=None, conf_n=0): """ Use a bonded + repulsive force field to generate 3D structure for a species. If the initial coordinates are reasonable e.g. from a previously generated 3D structure then add random displacement vectors and minimise to generate a conformer. Otherwise add atoms to the box sequentially until all atoms have been added, which generates a qualitatively reasonable 3D geometry which should be optimised using a electronic structure method V(x) = Σ_bonds k(d - d0)^2 + Σ_ij c/d^n Arguments: species (autode.species.Species): dist_consts (dict): Key = tuple of atom indexes, Value = distance conf_n (int): Number of this conformer Returns: (list(autode.atoms.Atom)): Atoms """ xyz_filename = f'{species.name}_conf{conf_n}_siman.xyz' saved_atoms = get_atoms_from_generated_file(species, xyz_filename) if saved_atoms is not None: return saved_atoms # To generate the potential requires bonds between atoms defined in a # molecular graph if species.graph is None: raise NoMolecularGraph # Initialise a new random seed and make a copy of the species' atoms. # RandomState is thread safe rand = np.random.RandomState() atoms = get_atoms_rotated_stereocentres(species=species, atoms=deepcopy(species.atoms), rand=rand) # Add the distance constraints as fixed bonds d0 = get_ideal_bond_length_matrix(atoms=species.atoms, bonds=species.graph.edges()) # Add distance constraints across stereocentres e.g. for a Z double bond # then modify d0 appropriately dist_consts = add_dist_consts_for_stereocentres( species=species, dist_consts={} if dist_consts is None else dist_consts) constrained_bonds = [] for bond, length in dist_consts.items(): i, j = bond d0[i, j] = length d0[j, i] = length constrained_bonds.append(bond) # Randomise coordinates that aren't fixed by shifting a maximum of # autode.Config.max_atom_displacement in x, y, z fixed_atom_indexes = get_non_random_atoms(species=species) # Shift by a factor defined in the config file if the coordinates are # reasonable but otherwise init in a 10 A cube initial_coords_are_reasonable = are_coords_reasonable( species.get_coordinates()) if initial_coords_are_reasonable: factor = Config.max_atom_displacement / np.sqrt(3) [ atom.translate(vec=factor * rand.uniform(-1, 1, 3)) for i, atom in enumerate(atoms) if i not in fixed_atom_indexes ] else: # Randomise in a 10 Å cubic box [atom.translate(vec=rand.uniform(-5, 5, 3)) for atom in atoms] logger.info('Minimising species...') st = time() if initial_coords_are_reasonable: coords = get_coords_minimised_v(coords=np.array( [atom.coord for atom in atoms]), bonds=species.graph.edges, k=1.0, c=0.01, d0=d0, tol=1E-5, fixed_bonds=constrained_bonds) else: coords = get_coords_no_init_strucutre(atoms, species, d0, constrained_bonds) logger.info(f' ... ({time()-st:.3f} s)') # Set the coordinates of the new atoms for i, atom in enumerate(atoms): atom.coord = coords[i] # Print an xyz file so rerunning will read the file atoms_to_xyz_file(atoms=atoms, filename=xyz_filename) return atoms
def add_dist_consts_for_stereocentres(species, dist_consts): """ Add distances constraints across two bonded stereocentres, for example for a Z alkene, (hopefully) ensuring that in the conformer generation the stereochemistry is retained. Will also add distance constraints from one nearest neighbour to the other nearest neighbours for that chiral centre Arguments: species (autode.species.Species): dist_consts (dict): keyed with tuple of atom indexes and valued with the distance (Å), or None Returns: (dict): Distance constraints """ if not are_coords_reasonable(coords=species.get_coordinates()): # TODO generate a reasonable initial structure: molassembler? logger.error('Cannot constrain stereochemistry if the initial ' 'structure is not sensible') return dist_consts stereocentres = [ node for node in species.graph.nodes if species.graph.nodes[node]['stereo'] is True ] # Get the stereocentres with 4 bonds as ~ chiral centres chiral_centres = [ centre for centre in stereocentres if len(list(species.graph.neighbors(centre))) == 4 ] # Add distance constraints from one atom to the other 3 atoms to fix the # configuration for chiral_centre in chiral_centres: neighbors = list(species.graph.neighbors(chiral_centre)) atom_i = neighbors[0] for atom_j in neighbors[1:]: dist_consts[(atom_i, atom_j)] = species.get_distance(atom_i, atom_j) # Check on every pair of stereocenters for (atom_i, atom_j) in combinations(stereocentres, 2): # If they are not bonded don't alter if (atom_i, atom_j) not in species.graph.edges: continue # Add a single distance constraint between the nearest neighbours of # each stereocentre for atom_i_neighbour in species.graph.neighbors(atom_i): for atom_j_neighbour in species.graph.neighbors(atom_j): if atom_i_neighbour != atom_j and atom_j_neighbour != atom_i: # Fix the distance to the current value dist_consts[(atom_i_neighbour, atom_j_neighbour)] = species.get_distance( atom_i_neighbour, atom_j_neighbour) logger.info(f'Have {len(dist_consts)} distance constraint(s)') return dist_consts