def confab_operator(filename, options, logfunction=None): ''' ''' if logfunction is not None: logfunction( f'--> Performing conformational search and optimization on {filename}' ) data = read_xyz(filename) if len(data.atomcoords) > 1: raise InputError( f'Requested conformational search on file {filename} that already contains more than one structure.' ) if len( tuple( connected_components(graphize(data.atomcoords[0], data.atomnos)))) > 1: raise InputError(( f'Requested conformational search on a molecular complex (file {filename}). ' 'Confab is not suited for this task, and using TSCoDe\'s csearch> operator ' 'is a better idea.')) if len(set(data.atomnos) - {1, 6, 7, 8, 9, 15, 16, 17, 35, 53}) != 0: raise InputError(( 'Requested conformational search on a molecule that contains atoms different ' 'than the ones for which OpenBabel Force Fields are parametrized. Please ' 'perform this conformational search through the more sophisticated and better ' 'integrated csearch> operator, part of the TSCoDe program.')) confname = filename[:-4] + '_confab.xyz' with suppress_stdout_stderr(): check_call( f'obabel {filename} -O {confname} --confab --rcutoff 0.5 --original' .split(), stdout=DEVNULL, stderr=STDOUT) # running Confab through Openbabel data = read_xyz(confname) conformers = data.atomcoords if len(conformers) > 10 and not options.let: conformers = conformers[0:10] logfunction( f'Will use only the best 10 conformers for TSCoDe embed.\n') os.remove(confname) with open(confname, 'w') as f: for i, conformer in enumerate(conformers): write_xyz(conformer, data.atomnos, f, title=f'Generated conformer {i}') return confname
def _set_reactive_indexes(self, filename): ''' Manually set the molecule reactive atoms from the ASE GUI, imposing constraints on the desired atoms. ''' from ase import Atoms from ase.gui.gui import GUI from ase.gui.images import Images data = read_xyz(filename) coords = data.atomcoords[0] labels = ''.join([pt[i].symbol for i in data.atomnos]) atoms = Atoms(labels, positions=coords) while atoms.constraints == []: print(( '\nPlease, manually select the reactive atom(s) for molecule %s.' '\nRotate with right click and select atoms by clicking. Multiple selections can be done by Ctrl+Click.' '\nWith desired atom(s) selected, go to Tools -> Constraints -> Constrain, then close the GUI.' ) % (filename)) GUI(images=Images([atoms]), show_bonds=True).run() return list(atoms.constraints[0].get_indices())
def opt_operator(filename, embedder, logfunction=None): ''' ''' if logfunction is not None: logfunction( f'--> Performing {embedder.options.calculator} {embedder.options.theory_level} optimization on {filename}' ) t_start = time.perf_counter() data = read_xyz(filename) conformers = data.atomcoords energies = [] lowest_calc = _get_lowest_calc(embedder) conformers, energies = _refine_structures( conformers, data.atomnos, *lowest_calc, loadstring='Optimizing conformer') energies, conformers = zip( *sorted(zip(energies, conformers), key=lambda x: x[0])) energies = np.array(energies) - np.min(energies) conformers = np.array(conformers) # sorting structures based on energy mask = energies < 10 # getting the structures to reject (Rel Energy > 10 kcal/mol) if logfunction is not None: s = 's' if len(conformers) > 1 else '' s = f'Completed optimization on {len(conformers)} conformer{s}. ({time_to_string(time.perf_counter()-t_start)}).\n' if max(energies) > 10: s += f'Discarded {len(conformers)-np.count_nonzero(mask)}/{len(conformers)} unstable conformers (Rel. E. > 10 kcal/mol)\n' conformers, energies = conformers[mask], energies[mask] # applying the mask that rejects high energy confs optname = filename[:-4] + '_opt.xyz' with open(optname, 'w') as f: for i, conformer in enumerate(conformers): write_xyz( conformer, data.atomnos, f, title= f'Optimized conformer {i} - Rel. E. = {round(energies[i], 3)} kcal/mol' ) logfunction(s + '\n') return optname
def neb_operator(filename, embedder): ''' ''' embedder.t_start_run = time.perf_counter() data = read_xyz(filename) assert len( data.atomcoords ) == 2, 'NEB calculations need a .xyz input file with two geometries.' from tscode.ase_manipulations import ase_neb, ase_popt reagents, products = data.atomcoords title = filename[:-4] + '_NEB' embedder.log( f'--> Performing a NEB TS optimization. Using start and end points from {filename}\n' f'Theory level is {embedder.options.theory_level} via {embedder.options.calculator}' ) print('Getting start point energy...', end='\r') _, reag_energy, _ = ase_popt(embedder, reagents, data.atomnos, steps=0) print('Getting end point energy...', end='\r') _, prod_energy, _ = ase_popt(embedder, products, data.atomnos, steps=0) ts_coords, ts_energy, _ = ase_neb(embedder, reagents, products, data.atomnos, title=title, logfunction=embedder.log, write_plot=True, verbose_print=True) e1 = ts_energy - reag_energy e2 = ts_energy - prod_energy embedder.log( f'NEB completed, relative energy from start/end points (not barrier heights):\n' f' > E(TS)-E(start): {"+" if e1>=0 else "-"}{round(e1, 3)} kcal/mol\n' f' > E(TS)-E(end) : {"+" if e2>=0 else "-"}{round(e2, 3)} kcal/mol') if not (e1 > 0 and e2 > 0): embedder.log( f'\nNEB failed, TS energy is lower than both the start and end points.\n' ) with open('Me_CONMe2_Mal_tetr_int_NEB_NEB_TS.xyz', 'w') as f: write_xyz(ts_coords, data.atomnos, f, title='NEB TS - see log for relative energies')
def run_tests(): import os import time from subprocess import CalledProcessError os.chdir(os.path.dirname(os.path.realpath(__file__))) from tscode.settings import (CALCULATOR, COMMANDS, DEFAULT_FF_LEVELS, DEFAULT_LEVELS, FF_CALC, FF_OPT_BOOL, PROCS) if CALCULATOR not in ('MOPAC','ORCA','GAUSSIAN','XTB'): raise Exception(f'{CALCULATOR} is not a valid calculator. Use MOPAC, ORCA, GAUSSIAN or XTB.') import numpy as np from ase.atoms import Atoms from ase.optimize import LBFGS from tscode.ase_manipulations import get_ase_calc from tscode.optimization_methods import opt_funcs_dict from tscode.utils import (HiddenPrints, clean_directory, loadbar, read_xyz, run_command, time_to_string) os.chdir('tests') t_start_run = time.perf_counter() data = read_xyz('C2H4.xyz') ########################################################################## print('\nRunning tests for TSCoDe. Settings used:') print(f'{CALCULATOR=}') if CALCULATOR != 'XTB': print(f'{CALCULATOR} COMMAND = {COMMANDS[CALCULATOR]}') print('\nTesting calculator...') ########################################################################## opt_funcs_dict[CALCULATOR](data.atomcoords[0], data.atomnos, method=DEFAULT_LEVELS[CALCULATOR], procs=PROCS, read_output=False) print(f'{CALCULATOR} raw calculator works.') ########################################################################## atoms = Atoms('HH', positions=np.array([[0, 0, 0], [0, 0, 1]])) atoms.calc = get_ase_calc((CALCULATOR, DEFAULT_LEVELS[CALCULATOR], PROCS, None)) LBFGS(atoms, logfile=None).run() clean_directory() print(f'{CALCULATOR} ASE calculator works.') ########################################################################## print(f'\n{FF_OPT_BOOL=}') ff = f'on. Calculator is {FF_CALC}. Checking its status.' if FF_OPT_BOOL else 'off.' print(f'Force Field optimization is turned {ff}') if FF_OPT_BOOL: if FF_CALC == 'OB': try: print('Trying to import the OpenBabel Python Module...') from openbabel import openbabel print('Module imported successfully.') except ImportError: raise Exception(f'Could not import OpenBabel Python module. Is standalone openbabel correctly installed?') else: # == 'XTB', 'GAUSSIAN' opt_funcs_dict[FF_CALC](data.atomcoords[0], data.atomnos, method=DEFAULT_FF_LEVELS[FF_CALC], procs=PROCS, read_output=False) print(f'{FF_CALC} FF raw calculator works.') ########################################################################## atoms.calc = get_ase_calc((FF_CALC, DEFAULT_FF_LEVELS[FF_CALC], PROCS, None)) LBFGS(atoms, logfile=None).run() clean_directory() print(f'{FF_CALC} ASE calculator works.') print('\nNo installation faults detected with the current settings. Running tests.') ########################################################################## tests = [] for f in os.listdir(): if f.endswith('.txt'): tests.append(os.path.realpath(f)) # os.chdir(os.path.dirname(os.getcwd())) # os.chdir('tscode') # # Back to ./tscode times = [] for i, f in enumerate(tests): name = f.split('\\')[-1].split('/')[-1][:-4] # trying to make it work for either Win, Linux (and Mac?) loadbar(i, len(tests), f'Running TSCoDe tests ({name}): ') t_start = time.perf_counter() try: with HiddenPrints(): run_command(f'python -m tscode {f} -n {name}') except CalledProcessError as error: print('\n\n--> An error occurred:\n') print(error.stderr.decode("utf-8")) quit() t_end = time.perf_counter() times.append(t_end-t_start) loadbar(len(tests), len(tests), f'Running TSCoDe tests ({name}): ') print() for i, f in enumerate(tests): print(' {:25s}{} s'.format(f.split('\\')[-1].split('/')[-1][:-4], round(times[i], 3))) print(f'\nTSCoDe tests completed with no errors. ({time_to_string(time.perf_counter() - t_start_run)})\n')
def __init__(self, filename, reactive_indexes=None, debug=False): ''' Initializing class properties: reading conformational ensemble file, aligning conformers to first and centering them in origin. :params filename: Input file name. Can be anything, .xyz preferred :params reactive_indexes: Index of atoms that will link during the desired reaction. May be either int or list of int. :params hyper: bool, whether to calculate orbitals positions ''' if not os.path.isfile(filename): if '.' in filename: raise SyntaxError(( f'Molecule {filename} cannot be read. Please check your syntax.' )) raise SyntaxError(( f'The program is trying to read something that is not a valid molecule input ({filename}). ' + 'If this looks like a keyword, it is probably faulted by a syntax error.' )) self.rootname = filename.split('.')[0] self.name = filename self.debug = debug if isinstance(reactive_indexes, np.ndarray): self.reactive_indexes = reactive_indexes else: self.reactive_indexes = np.array(reactive_indexes) if isinstance( reactive_indexes, (tuple, list)) else () ccread_object = read_xyz(filename) if ccread_object is None: raise CCReadError(f'Cannot read file {filename}') coordinates = np.array(ccread_object.atomcoords) # if coordinates.shape[0] > 5: # coordinates = coordinates[0:5] # # Do not keep more than 5 conformations self.atomnos = ccread_object.atomnos self.position = np.array([0, 0, 0], dtype=float) # used in Embedder class self.rotation = np.identity( 3) # used in Embedder class - rotation matrix assert all([ len(coordinates[i]) == len(coordinates[0]) for i in range(1, len(coordinates)) ]), 'Ensembles must have constant atom number.' # Checking that ensemble has constant length if self.debug: print( f'DEBUG--> Initializing object {filename}\nDEBUG--> Found {len(coordinates)} structures with {len(coordinates[0])} atoms' ) self.centroid = np.sum(np.sum(coordinates, axis=0), axis=0) / ( len(coordinates) * len(coordinates[0])) if self.debug: print('DEBUG--> Centroid was', self.centroid) self.atomcoords = coordinates - self.centroid self.graph = graphize(self.atomcoords[0], self.atomnos) # show_graph(self) self.atoms = np.array([ atom for structure in self.atomcoords for atom in structure ]) # single list with all atomic positions if self.debug: print(f'DEBUG--> Total of {len(self.atoms)} atoms')
def xtb_opt(coords, atomnos, constrained_indexes=None, method='GFN2-xTB', solvent=None, title='temp', read_output=True, **kwargs): ''' This function writes an XTB .inp file, runs it with the subprocess module and reads its output. :params coords: array of shape (n,3) with cartesian coordinates for atoms. :params atomnos: array of atomic numbers for atoms. :params constrained_indexes: array of shape (n,2), with the indexes of atomic pairs to be constrained. :params method: string, specifiyng the theory level to be used. :params title: string, used as a file name and job title for the mopac input file. :params read_output: Whether to read the output file and return anything. ''' with open(f'{title}.xyz', 'w') as f: write_xyz(coords, atomnos, f, title=title) s = f'$opt\n logfile={title}_opt.log\n$end' if constrained_indexes is not None: s += '\n$constrain\n' for a, b in constrained_indexes: s += ' distance: %s, %s, %s\n' % ( a + 1, b + 1, round(norm_of(coords[a] - coords[b]), 5)) if method.upper() in ('GFN-XTB', 'GFNXTB'): s += '\n$gfn\n method=1\n' elif method.upper() in ('GFN2-XTB', 'GFN2XTB'): s += '\n$gfn\n method=2\n' s += '\n$end' s = ''.join(s) with open(f'{title}.inp', 'w') as f: f.write(s) flags = '--opt' if method in ('GFN-FF', 'GFNFF'): flags += ' tight' # tighter convergence for GFN-FF works better flags += ' --gfnff' # declaring the use of FF instead of semiempirical if solvent is not None: if solvent == 'methanol': flags += f' --gbsa methanol' else: flags += f' --alpb {solvent}' elif method.upper() in ('GFN-FF', 'GFNFF'): flags += f' --alpb thf' try: check_call( f'xtb --input {title}.inp {title}.xyz {flags} > temp.log 2>&1'. split(), stdout=DEVNULL, stderr=STDOUT) except KeyboardInterrupt: print('KeyboardInterrupt requested by user. Quitting.') quit() if read_output: try: outname = 'xtbopt.xyz' opt_coords = read_xyz(outname).atomcoords[0] energy = read_xtb_energy(outname) clean_directory() os.remove(outname) for filename in ('gfnff_topo', 'charges', 'wbo', 'xtbrestart', 'xtbtopo.mol', '.xtboptok'): try: os.remove(filename) except FileNotFoundError: pass return opt_coords, energy, True except FileNotFoundError: return None, None, False
def csearch_operator(filename, embedder): ''' ''' embedder.log(f'--> Performing conformational search on {filename}') t_start = time.perf_counter() data = read_xyz(filename) if len(data.atomcoords) > 1: embedder.log( f'Requested conformational search on multimolecular file - will do\n' + 'an individual search from each conformer (might be time-consuming).' ) calc, method, procs = _get_lowest_calc(embedder) conformers = [] for i, coords in enumerate(data.atomcoords): opt_coords = optimize( coords, data.atomnos, calculator=calc, method=method, procs=procs)[0] if embedder.options.optimization else coords # optimize starting structure before running csearch conf_batch = clustered_csearch(opt_coords, data.atomnos, title=f'{filename}, conformer {i+1}', logfunction=embedder.log) # generate the most diverse conformers starting from optimized geometry conformers.append(conf_batch) conformers = np.array(conformers) batch_size = conformers.shape[1] conformers = conformers.reshape(-1, data.atomnos.shape[0], 3) # merging structures from each run in a single array if embedder.embed is not None: embedder.log( f'\nSelected the most diverse {batch_size} out of {conformers.shape[0]} conformers for {filename} ({time_to_string(time.perf_counter()-t_start)})' ) conformers = most_diverse_conformers(batch_size, conformers, data.atomnos) confname = filename[:-4] + '_confs.xyz' with open(confname, 'w') as f: for i, conformer in enumerate(conformers): write_xyz(conformer, data.atomnos, f, title=f'Generated conformer {i}') # if len(conformers) > 10 and not embedder.options.let: # s += f' Will use only the best 10 conformers for TSCoDe embed.' # embedder.log(s) embedder.log('\n') return confname
def orca_opt(coords, atomnos, constrained_indexes=None, method='PM3', procs=1, solvent=None, title='temp', read_output=True): ''' This function writes an ORCA .inp file, runs it with the subprocess module and reads its output. :params coords: array of shape (n,3) with cartesian coordinates for atoms. :params atomnos: array of atomic numbers for atoms. :params constrained_indexes: array of shape (n,2), with the indexes of atomic pairs to be constrained. :params method: string, specifiyng the first line of keywords for the MOPAC input file. :params title: string, used as a file name and job title for the mopac input file. :params read_output: Whether to read the output file and return anything. ''' s = '! %s Opt\n\n# ORCA input generated by TSCoDe\n\n' % (method) if solvent is not None: s += '\n' + get_solvent_line(solvent, 'ORCA', method) + '\n' if procs > 1: s += f'%pal nprocs {procs} end\n' if constrained_indexes is not None: s += f'%{""}geom\nConstraints\n' # weird f-string to prevent python misinterpreting % for a, b in constrained_indexes: s += '{B %s %s C}\n' % (a, b) s += 'end\nend\n\n' s += '*xyz 0 1\n' for i, atom in enumerate(coords): s += '%s % .6f % .6f % .6f\n' % (pt[atomnos[i]].symbol, atom[0], atom[1], atom[2]) s += '*\n' s = ''.join(s) with open(f'{title}.inp', 'w') as f: f.write(s) try: check_call(f'{COMMANDS["ORCA"]} {title}.inp'.split(), stdout=DEVNULL, stderr=STDOUT) except KeyboardInterrupt: print('KeyboardInterrupt requested by user. Quitting.') quit() if read_output: try: opt_coords = read_xyz(f'{title}.xyz').atomcoords[0] energy = read_orca_property(f'{title}_property.txt') clean_directory() return opt_coords, energy, True except FileNotFoundError: return None, None, False