def test_find_torsions(self): terminal_torsions, non_terminal_torsions = find_terminal_torsions( self.conformer) self.assertEqual(len(self.conformer.torsions), len(terminal_torsions + non_terminal_torsions)) self.assertEqual(len(terminal_torsions), 1) # only one terminal methyl group self.assertEqual(len(non_terminal_torsions), 3) # N-C, O-C, C-C
def find_all_combos(conformer, delta=float(30), cistrans=True, chiral_centers=True): """ A function to find all possible conformer combinations for a given conformer """ terminal_torsions, torsions = find_terminal_torsions(conformer) cistranss = conformer.cistrans chiral_centers = conformer.chiral_centers torsion_angles = np.arange(0, 360, delta) torsion_combos = list( itertools.combinations_with_replacement(torsion_angles, len(torsions))) if len(torsions) != 1: torsion_combos = list( set(torsion_combos + list( itertools.combinations_with_replacement( torsion_angles[::-1], len(torsions))))) if cistrans: cistrans_options = ["E", "Z"] cistrans_combos = list( itertools.combinations_with_replacement(cistrans_options, len(cistranss))) if len(cistranss) != 1: cistrans_combos = list( set(cistrans_combos + list( itertools.combinations_with_replacement( cistrans_options[::-1], len(cistranss))))) else: cistrans_combos = [()] if chiral_centers: chiral_options = ["R", "S"] chiral_combos = list( itertools.combinations_with_replacement(chiral_options, len(chiral_centers))) if len(chiral_centers) != 1: chiral_combos = list( set(chiral_combos + list( itertools.combinations_with_replacement( chiral_options[::-1], len(chiral_centers))))) else: chiral_combos = [()] all_combos = list( itertools.product(torsion_combos, cistrans_combos, chiral_combos)) return all_combos
def find_all_combos( conformer, delta=float(120), cistrans=True, chiral_centers=True): """ A function to find all possible conformer combinations for a given conformer Params: - conformer (`Conformer`) an AutoTST `Conformer` object of interest - delta (int or float): a number between 0 and 180 or how many conformers to generate per dihedral - cistrans (bool): indication of if one wants to consider cistrans bonds - chiral_centers (bool): indication of if one wants to consider chiral centers bonds Returns: - all_combos (list): a list corresponding to the number of unique conformers created. """ conformer.get_geometries() _, torsions = find_terminal_torsions(conformer) cistranss = conformer.cistrans chiral_centers = conformer.chiral_centers torsion_angles = np.arange(0, 360, delta) torsion_combos = list(itertools.product( torsion_angles, repeat=len(torsions))) if cistrans: cistrans_options = ["E", "Z"] cistrans_combos = list(itertools.product( cistrans_options, repeat=len(cistranss))) else: cistrans_combos = [()] if chiral_centers: chiral_options = ["R", "S"] chiral_combos = list(itertools.product( chiral_options, repeat=len(chiral_centers))) else: chiral_combos = [()] all_combos = list( itertools.product( torsion_combos, cistrans_combos, chiral_combos)) return all_combos
def systematic_search( conformer, delta=float(60), cistrans=True, chiral_centers=True, ): """ Perfoms a systematic conformer analysis of a `Conformer` or a `TS` object Variables: - conformer (`Conformer` or `TS`): a `Conformer` or `TS` object of interest - delta (int or float): a number between 0 and 180 or how many conformers to generate per dihedral - cistrans (bool): indication of if one wants to consider cistrans bonds - chiral_centers (bool): indication of if one wants to consider chiral centers bonds Returns: - confs (list): a list of unique `Conformer` objects within 1 kcal/mol of the lowest energy conformer determined """ # Takes each of the molecule objects combos = find_all_combos(conformer, delta=delta, cistrans=cistrans, chiral_centers=chiral_centers) if len(combos) == 0: logging.info( "This species has no torsions, cistrans bonds, or chiral centers") logging.info("Returning origional conformer") return [conformer] _, torsions = find_terminal_torsions(conformer) calc = conformer.ase_molecule.get_calculator() if isinstance(calc, FileIOCalculator): logging.info("The calculator generates input and output files.") results = [] conformers = {} combinations = {} for index, combo in enumerate(combos): combinations[index] = combo torsions, cistrans, chiral_centers = combo for i, torsion in enumerate(torsions): tor = conformer.torsions[i] i, j, k, l = tor.atom_indices mask = tor.mask conformer.ase_molecule.set_dihedral(a1=i, a2=j, a3=k, a4=l, angle=torsion, mask=mask) conformer.update_coords() for i, e_z in enumerate(cistrans): ct = conformer.cistrans[i] conformer.set_cistrans(ct.index, e_z) for i, s_r in enumerate(chiral_centers): center = conformer.chiral_centers[i] conformer.set_chirality(center.index, s_r) conformer.update_coords_from("ase") conformers[index] = conformer.copy() logging.info("There are {} unique conformers generated".format( len(conformers))) def opt_conf(conformer, calculator, i): """ A helper function to optimize the geometry of a conformer. Only for use within this parent function """ labels = [] for bond in conformer.bonds: labels.append(bond.atom_indices) if isinstance(conformer, TS): label = conformer.reaction_label ind1 = conformer.rmg_molecule.getLabeledAtom("*1").sortingLabel ind2 = conformer.rmg_molecule.getLabeledAtom("*3").sortingLabel labels.append([ind1, ind2]) type = 'ts' else: label = conformer.smiles type = 'species' if isinstance(calc, FileIOCalculator): if calculator.directory: directory = calculator.directory else: directory = 'conformer_logs' calculator.label = "{}_{}".format(conformer.smiles, i) calculator.directory = os.path.join( directory, label, '{}_{}'.format(conformer.smiles, i)) if not os.path.exists(calculator.directory): try: os.makedirs(calculator.directory) except OSError: logging.info("An error occured when creating {}".format( calculator.directory)) calculator.atoms = conformer.ase_molecule from ase.constraints import FixBondLengths c = FixBondLengths(labels) conformer.ase_molecule.set_constraint(c) conformer.ase_molecule.set_calculator(calculator) opt = BFGS(conformer.ase_molecule, logfile=None) opt.run() conformer.update_coords_from("ase") energy = get_energy(conformer) return_dict[i] = (energy, conformer.ase_molecule.arrays, conformer.ase_molecule.get_all_distances()) manager = Manager() return_dict = manager.dict() processes = [] for i, conf in list(conformers.items()): p = Process(target=opt_conf, args=(conf, calc, i)) processes.append(p) active_processes = [] for process in processes: if len(active_processes) < multiprocessing.cpu_count(): process.start() active_processes.append(process) continue else: one_done = False while not one_done: for i, p in enumerate(active_processes): if not p.is_alive(): one_done = True break process.start() active_processes[i] = process complete = np.zeros_like(active_processes, dtype=bool) while not np.all(complete): for i, p in enumerate(active_processes): if not p.is_alive(): complete[i] = True from ase import units results = [] for _, values in list(return_dict.items()): results.append(values) df = pd.DataFrame(results, columns=["energy", "arrays", 'distances']) df = df[df.energy < df.energy.min() + units.kcal / units.mol / units.eV].sort_values("energy") tolerance = 0.1 scratch_index = [] unique_index = [] for index in df.index: if index in scratch_index: continue unique_index.append(index) scratch_index.append(index) distances = df.distances[index] for other_index in df.index: if other_index in scratch_index: continue other_distances = df.distances[other_index] if tolerance > np.sqrt((distances - other_distances)**2).mean(): scratch_index.append(other_index) logging.info("We have identified {} unique conformers for {}".format( len(unique_index), conformer)) confs = [] i = 0 for info in df[["energy", "arrays"]].loc[unique_index].values: copy_conf = conformer.copy() energy, array = info copy_conf.energy = energy copy_conf.ase_molecule.set_positions(array["positions"]) copy_conf.update_coords_from("ase") c = copy_conf.copy() c.index = i confs.append(c) i += 1 return confs
def perform_simple_es(autotst_object, initial_pop=None, top_percent=0.3, min_rms=60, max_generations=500, store_generations=False, store_directory=".", delta=30): """ Performs evolutionary strategy to determine the lowest energy conformer of a TS or molecule. :param autotst_object: an autotst_ts, autotst_rxn, or autotst_molecule that you want to perform conformer analysis on * the ase_object of the autotst_object must have a calculator attached to it. :param initial_pop: a DataFrame containing the initial population :param top_percent: float of the top percentage of conformers you want to select :param min_rms: float of one of the possible cut off points for the analysis :param max_generations: int of one of the possible cut off points for the analysis :param store_generations: do you want to store pickle files of each generation :param store_directory: the director where you want the pickle files stored :param delta: the degree change in dihedral angle between each possible dihedral angle :return results: a DataFrame containing the final generation :return unique_conformers: a dictionary with indicies of unique torsion combinations and entries of energy of those torsions """ assert autotst_object, "No AutoTST object provided..." if initial_pop is None: logging.info( "No initial population provided, creating one using base parameters..." ) initial_pop = create_initial_population(autotst_object, delta=delta) possible_dihedrals = np.arange(0, 360, delta) top = select_top_population(initial_pop, top_percent=top_percent) population_size = initial_pop.shape[0] results = initial_pop if isinstance(autotst_object, autotst.species.Species): logging.info("The object given is a `Molecule` object") torsions = autotst_object.torsions ase_object = autotst_object.ase_molecule label = autotst_object.smiles if isinstance(autotst_object, autotst.reaction.Reaction): logging.info("The object given is a `Reaction` object") torsions = autotst_object.ts.torsions ase_object = autotst_object.ts.ase_ts label = autotst_object.label if isinstance(autotst_object, autotst.reaction.TS): logging.info("The object given is a `TS` object") torsions = autotst_object.torsions ase_object = autotst_object.ase_ts label = autotst_object.label assert ase_object.get_calculator( ), "To use ES, you must attach an ASE calculator to the `ase_molecule`." gen_number = 0 complete = False unique_conformers = {} terminal_torsions, non_terminal_torsions = find_terminal_torsions( autotst_object) while complete == False: relaxed_top = [] for combo in top.iloc[:, 1:].values: for index, torsion in enumerate(non_terminal_torsions): i, j, k, l = torsion.indices right_mask = torsion.right_mask dihedral = combo[index] ase_object.set_dihedral(a1=i, a2=j, a3=k, a4=l, angle=float(dihedral), mask=right_mask) update_from_ase(autotst_object) relaxed_e, relaxed_object = partial_optimize_mol(autotst_object) new_dihedrals = [] for torsion in non_terminal_torsions: i, j, k, l = torsion.indices right_mask = torsion.right_mask d = relaxed_object.get_dihedral(a1=i, a2=j, a3=k, a4=l) new_dihedrals.append(d) relaxed_top.append([relaxed_e] + new_dihedrals) columns = top.columns top = pd.DataFrame(relaxed_top, columns=columns) if store_generations: save_name = "{}_relaxed_top_es_generation_{}.csv".format( label, gen_number) f = os.path.join(store_directory, save_name) top.to_csv(f) gen_number += 1 logging.info("Performing ES on generation {}".format(gen_number)) r = [] for individual in range(population_size): dihedrals = [] for index, torsion in enumerate(non_terminal_torsions): i, j, k, l = torsion.indices right_mask = torsion.right_mask dihedral = random.gauss(top.mean()["torsion_" + str(index)], top.std()["torsion_" + str(index)]) dihedrals.append(dihedral) ase_object.set_dihedral(a1=i, a2=j, a3=k, a4=l, angle=float(dihedral), mask=right_mask) # Updating the molecule update_from_ase(autotst_object) energy = get_energy(autotst_object) r.append([energy] + dihedrals) results = pd.DataFrame(r) logging.info( "Creating the DataFrame of results for the {}th generation".format( gen_number)) results.columns = top.columns results = results.sort_values("energy") unique_conformers = get_unique_conformers(results, unique_conformers, min_rms) if store_generations: # This portion stores each generation if desired logging.info("Saving the results DataFrame") generation_name = "{0}_es_generation_{1}.csv".format( label, gen_number) f = os.path.join(store_directory, generation_name) results.to_csv(f) top = select_top_population(results, top_percent) best = top.iloc[0, 1:] worst = top.iloc[-1, 1:] rms = ((best - worst)**2).mean() if gen_number >= max_generations: complete = True logging.info("Max generations reached. ES complete.") if rms < min_rms: complete = True logging.info("Cutoff criteria reached. ES complete.") return results, unique_conformers
def systematic_search(conformer, delta=float(30), cistrans=True, chiral_centers=True, store_results=False, store_directory="."): """ Perfoms a brute force conformer analysis of a molecule or a transition state :param autotst_object: am autotst_ts, autotst_rxn, or autotst_molecule that you want to perform conformer analysis on * the ase_object of the autotst_object must have a calculator attached to it. :param store_generations: do you want to store pickle files of each generation :param store_directory: the director where you want the pickle files stored :param delta: the degree change in dihedral angle between each possible dihedral angle :return results: a DataFrame containing the final generation :return unique_conformers: a dictionary with indicies of unique torsion combinations and entries of energy of those torsions """ # Takes each of the molecule objects combos = find_all_combos(conformer, delta=delta, cistrans=cistrans, chiral_centers=chiral_centers) terminal_torsions, torsions = find_terminal_torsions(conformer) file_name = conformer.smiles + "_brute_force.csv" calc = conformer.ase_molecule.get_calculator() results = [] for combo in combos: torsions, cistrans, chiral_centers = combo for i, torsion in enumerate(torsions): tor = conformer.torsions[i] i, j, k, l = tor.atom_indices mask = tor.mask conformer.ase_molecule.set_dihedral(a1=i, a2=j, a3=k, a4=l, angle=torsion, mask=mask) conformer.update_coords() for i, e_z in enumerate(cistrans): ct = conformer.cistrans[i] conformer.set_cistrans(ct.index, e_z) for i, s_r in enumerate(chiral_centers): center = conformer.chiral_centers[i] conformer.set_chiral_center(center.index, s_r) conformer.ase_molecule.set_calculator(calc) ##### Something funky happening with this optimization #conformer.update_coords() #opt = BFGS(conformer.ase_molecule) #opt.run() conformer.update_coords() energy = get_energy(conformer) sample = ["torsion_{}", "cistrans_{}", "chiral_center_{}"] columns = ["energy"] long_combo = [energy] for c, name in zip(combo, sample): for i, info in enumerate(c): columns.append(name.format(i)) long_combo.append(info) results.append(long_combo) brute_force = pd.DataFrame(results, columns=columns) if store_results: f = os.path.join(store_directory, file_name) brute_force.to_csv(f) from ase import units unique_conformers = [] for i, ind in enumerate(brute_force[brute_force.energy < ( brute_force.energy.min() + units.kcal / units.mol / units.eV)].index): copy_conf = conformer.copy() copy_conf.index = i for col in brute_force.columns[1:]: geometry = col.split("_")[0] index = int(col.split("_")[-1]) value = brute_force.loc[ind][col] if "torsion" in geometry: copy_conf.set_torsion(index, float(value)) elif "cistrans" in geometry.lower(): copy_conf.set_cistrans(index, value) elif "chiral" in geometry.lower(): copy_conf.set_chirality(index, value) unique_conformers.append(copy_conf) return brute_force, unique_conformers
def systematic_search(conformer, delta=float(120), energy_cutoff = 10.0, #kcal/mol rmsd_cutoff = 0.5, #angstroms cistrans = True, chiral_centers = True, multiplicity = False, ): """ Perfoms a systematic conformer analysis of a `Conformer` or a `TS` object Variables: - conformer (`Conformer` or `TS`): a `Conformer` or `TS` object of interest - delta (int or float): a number between 0 and 180 or how many conformers to generate per dihedral - cistrans (bool): indication of if one wants to consider cistrans bonds - chiral_centers (bool): indication of if one wants to consider chiral centers bonds Returns: - confs (list): a list of unique `Conformer` objects within 1 kcal/mol of the lowest energy conformer determined """ rmsd_cutoff_options = { 'loose' : 1.0, 'default': 0.5, 'tight': 0.1 } energy_cutoff_options = { 'high' : 50.0, 'default' : 10.0, 'low' : 5.0 } if isinstance(rmsd_cutoff,str): rmsd_cutoff = rmsd_cutoff.lower() assert rmsd_cutoff in rmsd_cutoff_options.keys(), 'rmsd_cutoff options are loose, default, and tight' rmsd_cutoff = rmsd_cutoff_options[rmsd_cutoff] if isinstance(energy_cutoff,str): energy_cutoff = energy_cutoff.lower() assert energy_cutoff in energy_cutoff_options.keys(), 'energy_cutoff options are low, default, and high' energy_cutoff = energy_cutoff_options[energy_cutoff] if not isinstance(conformer, TS): reference_mol = conformer.rmg_molecule.copy(deep=True) reference_mol = reference_mol.to_single_bonds() manager = Manager() return_dict = manager.dict() pool = multiprocessing.Pool() def opt_conf(i, rmsd_cutoff): """ A helper function to optimize the geometry of a conformer. Only for use within this parent function """ conformer = conformers[i] calculator = conformer.ase_molecule.get_calculator() labels = [] for bond in conformer.get_bonds(): labels.append(bond.atom_indices) if isinstance(conformer, TS): label = conformer.reaction_label ind1 = conformer.rmg_molecule.get_labeled_atoms("*1")[0].sorting_label ind2 = conformer.rmg_molecule.get_labeled_atoms("*3")[0].sorting_label labels.append([ind1, ind2]) type = 'ts' else: label = conformer.smiles type = 'species' if isinstance(calc, FileIOCalculator): if calculator.directory: directory = calculator.directory else: directory = 'conformer_logs' calculator.label = "{}_{}".format(conformer.smiles, i) calculator.directory = os.path.join(directory, label,'{}_{}'.format(conformer.smiles, i)) if not os.path.exists(calculator.directory): try: os.makedirs(calculator.directory) except OSError: logging.info("An error occured when creating {}".format(calculator.directory)) calculator.atoms = conformer.ase_molecule conformer.ase_molecule.set_calculator(calculator) opt = BFGS(conformer.ase_molecule, logfile=None) if type == 'species': if isinstance(i,int): c = FixBondLengths(labels) conformer.ase_molecule.set_constraint(c) try: opt.run(steps=1e6) except RuntimeError: logging.info("Optimization failed...we will use the unconverged geometry") pass if str(i) == 'ref': conformer.update_coords_from("ase") try: rmg_mol = Molecule() rmg_mol.from_xyz( conformer.ase_molecule.arrays["numbers"], conformer.ase_molecule.arrays["positions"] ) if not rmg_mol.is_isomorphic(reference_mol): logging.info("{}_{} is not isomorphic with reference mol".format(conformer,str(i))) return False except AtomTypeError: logging.info("Could not create a RMG Molecule from optimized conformer coordinates...assuming not isomorphic") return False if type == 'ts': c = FixBondLengths(labels) conformer.ase_molecule.set_constraint(c) try: opt.run(fmax=0.20, steps=1e6) except RuntimeError: logging.info("Optimization failed...we will use the unconverged geometry") pass conformer.update_coords_from("ase") energy = get_energy(conformer) conformer.energy = energy if len(return_dict)>0: conformer_copy = conformer.copy() for index,post in return_dict.items(): conf_copy = conformer.copy() conf_copy.ase_molecule.positions = post conf_copy.update_coords_from("ase") rmsd = rdMolAlign.GetBestRMS(conformer_copy.rdkit_molecule,conf_copy.rdkit_molecule) if rmsd <= rmsd_cutoff: return True if str(i) != 'ref': return_dict[i] = conformer.ase_molecule.get_positions() return True #if not isinstance(conformer,TS): # calc = conformer.ase_molecule.get_calculator() # reference_conformer = conformer.copy() # if opt_conf(reference_conformer, calc, 'ref', rmsd_cutoff): # conformer = reference_conformer combos = find_all_combos( conformer, delta=delta, cistrans=cistrans, chiral_centers=chiral_centers) if len(combos) == 0: logging.info( "This species has no torsions, cistrans bonds, or chiral centers") logging.info("Returning origional conformer") return [conformer] _, torsions = find_terminal_torsions(conformer) calc = conformer.ase_molecule.get_calculator() if isinstance(calc, FileIOCalculator): logging.info("The calculator generates input and output files.") results = [] global conformers conformers = {} combinations = {} logging.info("There are {} possible conformers to investigate...".format(len(combos))) for index, combo in enumerate(combos): combinations[index] = combo torsions, cistrans, chiral_centers = combo copy_conf = conformer.copy() for i, torsion in enumerate(torsions): tor = copy_conf.torsions[i] i, j, k, l = tor.atom_indices mask = tor.mask copy_conf.ase_molecule.set_dihedral( a1=i, a2=j, a3=k, a4=l, angle=torsion, mask=mask ) copy_conf.update_coords() for i, e_z in enumerate(cistrans): ct = copy_conf.cistrans[i] copy_conf.set_cistrans(ct.index, e_z) for i, s_r in enumerate(chiral_centers): center = copy_conf.chiral_centers[i] copy_conf.set_chirality(center.index, s_r) copy_conf.update_coords_from("ase") copy_conf.ase_molecule.set_calculator(calc) conformers[index] = copy_conf processes = [] for i, conf in list(conformers.items()): p = Process(target=opt_conf, args=(i, rmsd_cutoff)) processes.append(p) active_processes = [] for process in processes: if len(active_processes) < multiprocessing.cpu_count(): process.start() active_processes.append(process) continue else: one_done = False while not one_done: for i, p in enumerate(active_processes): if not p.is_alive(): one_done = True break process.start() active_processes[i] = process complete = np.zeros_like(active_processes, dtype=bool) while not np.all(complete): for i, p in enumerate(active_processes): if not p.is_alive(): complete[i] = True energies = [] for positions in list(return_dict.values()): conf = conformer.copy() conf.ase_molecule.positions = positions conf.ase_molecule.set_calculator(calc) energy = conf.ase_molecule.get_potential_energy() conf.update_coords_from("ase") energies.append((conf,energy)) df = pd.DataFrame(energies,columns=["conformer","energy"]) df = df[df.energy < df.energy.min() + (energy_cutoff * units.kcal / units.mol / units.eV)].sort_values("energy").reset_index(drop=True) redundant = [] conformer_copies = [conf.copy() for conf in df.conformer] for i,j in itertools.combinations(range(len(df.conformer)),2): copy_1 = conformer_copies[i].rdkit_molecule copy_2 = conformer_copies[j].rdkit_molecule rmsd = rdMolAlign.GetBestRMS(copy_1,copy_2) if rmsd <= rmsd_cutoff: redundant.append(j) redundant = list(set(redundant)) df.drop(df.index[redundant], inplace=True) if multiplicity and conformer.rmg_molecule.multiplicity > 2: rads = conformer.rmg_molecule.get_radical_count() if rads % 2 == 0: multiplicities = range(1,rads+2,2) else: multiplicities = range(2,rads+2,2) else: multiplicities = [conformer.rmg_molecule.multiplicity] confs = [] i = 0 for conf in df.conformer: if multiplicity: for mult in multiplicities: conf_copy = conf.copy() conf_copy.index = i conf_copy.rmg_molecule.multiplicity = mult confs.append(conf_copy) i += 1 else: conf.index = i confs.append(conf) i += 1 logging.info("We have identified {} unique, low-energy conformers for {}".format( len(confs), conformer)) return confs