def conf_is_unique_rmsd(conf, conf_list, rmsd_tol=None): """ Determine if a conformer is unique based on an root mean squared displacement RMSD threshold based on heavy atoms Arguments: conf (autode.conformer.Conformer): conf_list (list((list(autode.conformer.Conformer)): Keyword Arguments: rmsd_tol (float): Tolerance for an equivalent structure based on the rmsd in Å. If None then use the default value for autode.Config.rmsd_threshold Returns: (bool): """ rmsd_tol = Config.rmsd_threshold if rmsd_tol is None else rmsd_tol logger.info(f'Removing conformers with RMSD < {rmsd_tol} Å to any other') # Calculate the RMSD between this Conformer and the those in conf_list # using the Kabsch algorithm for other_conf in conf_list: if calc_heavy_atom_rmsd(conf.atoms, other_conf.atoms) < rmsd_tol: return False return True
def test_calc_rmsd(): atoms = [ Atom('C', 0.0009, 0.0041, -0.0202), Atom('H', -0.6577, -0.8481, -0.3214), Atom('H', -0.4585, 0.9752, -0.3061), Atom('H', 0.0853, -0.0253, 1.0804), Atom('H', 1.0300, -0.1058, -0.4327) ] atoms_rot = [ Atom('C', -0.0009, -0.0041, -0.0202), Atom('H', 0.6577, 0.8481, -0.3214), Atom('H', 0.4585, -0.9752, -0.3061), Atom('H', -0.0853, 0.0253, 1.0804), Atom('H', -1.0300, 0.1058, -0.4327) ] coords1 = np.array([atom.coord for atom in atoms]) coords2 = np.array([atom.coord for atom in atoms_rot]) # Rotated coordinates should have almost 0 RMSD between them assert geom.calc_rmsd(coords1, coords2) < 1E-5 # Coordinates need to have the same shape to calculate the RMSD with pytest.raises(AssertionError): _ = geom.calc_rmsd(coords1, coords2[1:]) assert geom.calc_heavy_atom_rmsd(atoms, atoms_rot) < 1E-5 # Permuting two hydrogens should generate a larger RMSD atoms_rot[2], atoms_rot[3] = atoms_rot[3], atoms_rot[2] rmsd = geom.calc_rmsd(coords1=np.array([atom.coord for atom in atoms]), coords2=np.array([atom.coord for atom in atoms_rot])) assert rmsd > 0.1 # While the heavy atom RMSD should remain unchanged assert geom.calc_heavy_atom_rmsd(atoms, atoms_rot) < 1E-6
def find_lowest_energy_ts_conformer(self, rmsd_threshold=None): """Find the lowest energy transition state conformer by performing constrained optimisations""" logger.info('Finding lowest energy TS conformer') atoms, energy = deepcopy(self.atoms), deepcopy(self.energy) calc = deepcopy(self.optts_calc) hmethod = get_hmethod() if Config.hmethod_conformers else None self.find_lowest_energy_conformer(hmethod=hmethod) # Remove similar TS conformer that are similar to this TS based on root # mean squared differences in their structures thresh = Config.rmsd_threshold if rmsd_threshold is None else rmsd_threshold self.conformers = [ conf for conf in self.conformers if calc_heavy_atom_rmsd(conf.atoms, atoms) > thresh ] logger.info(f'Generated {len(self.conformers)} unique (RMSD > ' f'{thresh} Å) TS conformer(s)') # Optimise the lowest energy conformer to a transition state - will # .find_lowest_energy_conformer will have updated self.atoms etc. if len(self.conformers) > 0: self.optimise(name_ext='optts_conf') if self.is_true_ts() and self.energy < energy: logger.info('Conformer search successful') return None # Ensure the energy has a numerical value, so a difference can be # evaluated self.energy = self.energy if self.energy is not None else 0 logger.warning(f'Transition state conformer search failed ' f'(∆E = {energy - self.energy:.4f} Ha). Reverting') logger.info('Reverting to previously found TS') self.atoms = atoms self.energy = energy self.optts_calc = calc self.imaginary_frequencies = calc.get_imaginary_freqs() return None