def parametrise(molecule): """Perform initial molecule parametrisation using OpenFF, Antechamber or XML.""" # First copy the pdb and any other files into the folder copy(f'../{molecule.filename}', f'{molecule.filename}') # Parametrisation options: param_dict = {'antechamber': AnteChamber, 'xml': XML} try: param_dict['openff'] = OpenFF except ImportError: pass # If we are using xml we have to move it if molecule.parameter_engine == 'xml': copy(f'../{molecule.name}.xml', f'{molecule.name}.xml') # Perform the parametrisation param_dict[molecule.parameter_engine](molecule) append_to_log( f'Parametrised molecule with {molecule.parameter_engine}') return molecule
def openmm_system(self): """Initialise the OpenMM system we will use to evaluate the energies.""" # Load the initial coords into the system and initialise pdb = app.PDBFile(self.pdb) forcefield = app.ForceField(self.xml) modeller = app.Modeller(pdb.topology, pdb.positions) # set the initial positions from the pdb self.system = forcefield.createSystem(modeller.topology, nonbondedMethod=app.NoCutoff, constraints=None) # Check what combination rule we should be using from the xml xmlstr = open(self.xml).read() # check if we have opls combination rules if the xml is present try: self.combination = ET.fromstring(xmlstr).find('NonbondedForce').attrib['combination'] append_to_log('OPLS combination rules found in xml file', msg_type='minor') except AttributeError: pass except KeyError: pass if self.combination == 'opls': self.opls_lj() temperature = 298.15 * unit.kelvin integrator = mm.LangevinIntegrator(temperature, 5 / unit.picoseconds, 0.001 * unit.picoseconds) self.simulation = app.Simulation(modeller.topology, self.system, integrator) self.simulation.context.setPositions(modeller.positions)
def stage_wrapper(self, start_key, begin_log_msg='', fin_log_msg='', torsion_options=None): """ Firstly, check if the stage start_key is in self.order; this tells you if the stage should be called or not. If it isn't in self.order: - Do nothing If it is: - Unpickle the ligand object at the start_key stage - Write to the log that something's about to be done (if specified) - Make (if not restarting) and / or move into the working directory for that stage - Do the thing - Move back out of the working directory for that stage - Write to the log that something's been done (if specified) - Pickle the ligand object again with the next_key marker as its stage """ mol = unpickle()[start_key] # Set the state for logging any exceptions should they arise mol.state = start_key # if we have a torsion options dictionary pass it to the molecule if torsion_options is not None: mol = self.store_torsions(mol, torsion_options) skipping = False if self.order[start_key] == self.skip: printf(f'Skipping stage: {start_key}') append_to_log(f'skipping stage: {start_key}') skipping = True else: if begin_log_msg: printf(f'{begin_log_msg}...', end=' ') home = os.getcwd() folder_name = f'{self.immutable_order.index(start_key) + 1}_{start_key}' # Make sure you don't get an error if restarting try: os.mkdir(folder_name) except FileExistsError: pass finally: os.chdir(folder_name) self.order[start_key](mol) self.order.pop(start_key, None) os.chdir(home) # Begin looping through self.order, but return after the first iteration. for key in self.order: next_key = key if fin_log_msg and not skipping: printf(fin_log_msg) mol.pickle(state=next_key) return next_key
def qm_optimise(self, molecule): """Optimise the molecule with or without geometric.""" # TODO This has gotten kinda gross, can we trim it and maybe move some logic back into engines.py? # TODO We initialise the PSI4 engine even if we're using QCEngine? qm_engine = self.engine_dict[molecule.bonds_engine](molecule) if molecule.geometric and molecule.bonds_engine == 'psi4': # Optimise the structure using QCEngine with geometric and psi4 qceng = QCEngine(molecule) result = qceng.call_qcengine('geometric', 'gradient', input_type='mm') # Check if converged and get the geometry if result['success']: # Load all of the frames into the molecules trajectory holder molecule.read_geometric_traj(result['trajectory']) # store the last frame as the qm optimised structure molecule.molecule['qm'] = molecule.molecule['traj'][-1] # Write out the trajectory file molecule.write_xyz(input_type='traj', name=f'{molecule.name}_opt') molecule.write_xyz(input_type='qm', name='opt') else: sys.exit('Molecule not optimised.') else: converged = qm_engine.generate_input(input_type='mm', optimise=True) # Check the exit status of the job; if failed restart the job up to 2 times restart_count = 1 while not converged and restart_count < 3: append_to_log( f'{molecule.bonds_engine} optimisation failed; restarting', msg_type='minor') converged = qm_engine.generate_input(input_type='mm', optimise=True, restart=True) restart_count += 1 if not converged: sys.exit( f'{molecule.bonds_engine} optimisation did not converge after 3 restarts; check log file.' ) molecule.molecule[ 'qm'], molecule.qm_energy = qm_engine.optimised_structure() molecule.write_xyz(input_type='qm', name='opt') append_to_log( f'qm_optimised structure calculated{" with geometric" if molecule.geometric else ""}' ) return molecule
def mod_sem(molecule): """Modified Seminario for bonds and angles.""" mod_sem = ModSeminario(molecule) mod_sem.modified_seminario_method() append_to_log('Mod_Seminario method complete') return molecule
def lennard_jones(molecule): """Calculate Lennard-Jones parameters, and extract virtual sites.""" os.system('cp ../7_charges/DDEC* .') lj = LennardJones(molecule) molecule.NonbondedForce = lj.calculate_non_bonded_force() append_to_log('Lennard-Jones parameters calculated') return molecule
def mod_sem(molecule): """Modified Seminario for bonds and angles.""" append_to_log('Starting mod_Seminario method') mod_sem = ModSeminario(molecule) mod_sem.modified_seminario_method() append_to_log('Finishing Mod_Seminario method') return molecule
def torsion_optimise(molecule): """Perform torsion optimisation.""" opt = TorsionOptimiser(molecule, refinement=molecule.refinement_method, vn_bounds=molecule.tor_limit) opt.run() append_to_log('Torsion_optimisations complete') return molecule
def lennard_jones(molecule): """Calculate Lennard-Jones parameters, and extract virtual sites.""" append_to_log('Starting Lennard-Jones parameter calculation') charges_fld = os.path.join(molecule.home, '7_charges') for file in os.listdir(charges_fld): if file.startswith('DDEC'): copy(os.path.join(charges_fld, file), file) lj = LennardJones(molecule) molecule.NonbondedForce = lj.calculate_non_bonded_force() append_to_log('Finishing Lennard-Jones parameter calculation') return molecule
def charges(molecule): """Perform DDEC calculation with Chargemol.""" # TODO add option to use chargemol on onetep cube files. # TODO Proper pathing copy(f'../6_density/{molecule.name}.wfx', f'{molecule.name}.wfx') c_mol = Chargemol(molecule) c_mol.generate_input() append_to_log( f'Charge analysis completed with Chargemol and DDEC{molecule.ddec_version}' ) return molecule
def mm_optimise(self, molecule): """ Use an mm force field to get the initial optimisation of a molecule options --------- RDKit MFF or UFF force fields can have strange effects on the geometry of molecules Geometric / OpenMM depends on the force field the molecule was parameterised with gaff/2, OPLS smirnoff. """ append_to_log('Starting mm_optimisation') # Check which method we want then do the optimisation if self.molecule.mm_opt_method == 'openmm': # Make the inputs molecule.write_pdb(input_type='input') molecule.write_parameters() # Run geometric # TODO Should this be moved to allow a decorator? with open('log.txt', 'w+') as log: sp.run( f'geometric-optimize --reset --epsilon 0.0 --maxiter {molecule.iterations} --pdb ' f'{molecule.name}.pdb --openmm {molecule.name}.xml ' f'{self.molecule.constraints_file if self.molecule.constraints_file is not None else ""}', shell=True, stdout=log, stderr=log) # This will continue even if we don't converge this is fine # Read the xyz traj and store the frames molecule.read_xyz(f'{molecule.name}_optim.xyz') # Store the last from the traj as the mm optimised structure molecule.coords['mm'] = molecule.coords['traj'][-1] else: # TODO change to qcengine as this can already be done # Run an rdkit optimisation with the right FF rdkit_ff = {'rdkit_mff': 'MFF', 'rdkit_uff': 'UFF'} molecule.filename = RDKit().mm_optimise( molecule.filename, ff=rdkit_ff[self.molecule.mm_opt_method]) append_to_log( f'Finishing mm_optimisation of the molecule with {self.molecule.mm_opt_method}' ) return molecule
def torsion_scan(molecule): """Perform torsion scan.""" scan = TorsionScan(molecule) # Try to find a scan file; if none provided and more than one torsion detected: prompt user try: copy('../../QUBE_torsions.txt', 'QUBE_torsions.txt') scan.find_scan_order(file='QUBE_torsions.txt') except FileNotFoundError: scan.find_scan_order() scan.start_scan() append_to_log('Torsion_scans complete') return molecule
def generate_input(self, execute=True): """Given a DDEC version (from the defaults), this function writes the job file for chargemol and executes it.""" if (self.molecule.ddec_version != 6) and (self.molecule.ddec_version != 3): append_to_log( message= 'Invalid or unsupported DDEC version given, running with default version 6.', msg_type='warning') self.molecule.ddec_version = 6 # Write the charges job file. with open('job_control.txt', 'w+') as charge_file: charge_file.write( f'<input filename>\n{self.molecule.name}.wfx\n</input filename>' ) charge_file.write('\n\n<net charge>\n0.0\n</net charge>') charge_file.write( '\n\n<periodicity along A, B and C vectors>\n.false.\n.false.\n.false.' ) charge_file.write('\n</periodicity along A, B and C vectors>') charge_file.write( f'\n\n<atomic densities directory complete path>\n{self.molecule.chargemol}' f'/atomic_densities/') charge_file.write('\n</atomic densities directory complete path>') charge_file.write( f'\n\n<charge type>\nDDEC{self.molecule.ddec_version}\n</charge type>' ) charge_file.write('\n\n<compute BOs>\n.true.\n</compute BOs>') if execute: with open('log.txt', 'w+') as log: # TODO Check if windows control_path = 'chargemol_FORTRAN_09_26_2017/compiled_binaries/linux/' \ 'Chargemol_09_26_2017_linux_serial job_control.txt' sp.run(os.path.join(self.molecule.chargemol, control_path), shell=True, stdout=log, stderr=log)
def charges(molecule): """Perform DDEC calculation with Chargemol.""" # TODO add option to use chargemol on onetep cube files. append_to_log('Starting charge partitioning') copy( os.path.join(molecule.home, os.path.join('6_density', f'{molecule.name}.wfx')), f'{molecule.name}.wfx') c_mol = Chargemol(molecule) c_mol.generate_input() append_to_log( f'Finishing Charge partitioning with Chargemol and DDEC{molecule.ddec_version}' ) return molecule
def torsion_optimise(molecule): """Perform torsion optimisation.""" append_to_log('Starting torsion_optimisations') # First we should make sure we have collected the results of the scans if not molecule.qm_scans: os.chdir(os.path.join(molecule.home, '9_torsion_scan')) scan = TorsionScan(molecule) scan.find_scan_order() scan.collect_scan() os.chdir(os.path.join(molecule.home, '10_torsion_optimise')) opt = TorsionOptimiser(molecule, refinement=molecule.refinement_method, vn_bounds=molecule.tor_limit) opt.run() append_to_log('Finishing torsion_optimisations') return molecule
def density(self, molecule): """Perform density calculation with the qm engine.""" append_to_log('Starting density calculation') if molecule.density_engine == 'onetep': molecule.write_xyz(input_type='qm') # If using ONETEP, stop after this step append_to_log('Density analysis file made for ONETEP') # Edit the order to end here self.order = OrderedDict([('density', self.density), ('charges', self.skip), ('lennard_jones', self.skip), ('torsion_scan', self.torsion_scan), ('pause', self.pause)]) else: qm_engine = self.engine_dict[molecule.density_engine](molecule) qm_engine.generate_input(input_type='qm', density=True, solvent=molecule.solvent) append_to_log('Finishing Density calculation') return molecule
def torsion_scan(molecule): """Perform torsion scan.""" # TODO find constraints file if present append_to_log('Starting torsion_scans') molecule.find_rotatable_dihedrals() molecule.symmetrise_from_topo() scan = TorsionScan(molecule) # Try to find a scan file; if none provided and more than one torsion detected: prompt user try: copy('../../QUBE_torsions.txt', 'QUBE_torsions.txt') scan.find_scan_order(file='QUBE_torsions.txt') except FileNotFoundError: scan.find_scan_order() scan.scan() append_to_log('Finishing torsion_scans') return molecule
def hessian(self, molecule): """Using the assigned bonds engine, calculate and extract the Hessian matrix.""" # TODO Because of QCEngine, nothing is being put into the hessian folder anymore # Need a way of writing QCEngine output to log file; still waiting on documentation ... molecule.get_bond_lengths(input_type='qm') # Check what engine we want to use if molecule.bonds_engine == 'g09': qm_engine = self.engine_dict[molecule.bonds_engine](molecule) qm_engine.generate_input(input_type='qm', hessian=True) molecule.hessian = qm_engine.hessian() else: qceng = QCEngine(molecule) molecule.hessian = qceng.call_qcengine('psi4', 'hessian', input_type='qm') append_to_log(f'Hessian calculated using {molecule.bonds_engine}') return molecule
def density(self, molecule): """Perform density calculation with the qm engine.""" if molecule.density_engine == 'onetep': molecule.write_xyz(input_type='qm') # If we use ONETEP we have to stop after this step append_to_log('Density analysis file made for ONETEP') # Now we have to edit the order to end here. self.order = OrderedDict([('density', self.density), ('charges', self.skip), ('lennard_jones', self.skip), ('torsion_scan', self.torsion_scan), ('pause', self.pause)]) else: # Do normal density calculation qm_engine = self.engine_dict[molecule.density_engine](molecule) qm_engine.generate_input(input_type='qm', density=True, solvent=molecule.solvent) append_to_log('Density analysis complete') return molecule
def parametrise(molecule): """Perform initial molecule parametrisation using OpenFF, Antechamber or XML.""" append_to_log('Starting parametrisation') # Write the PDB file this covers us if we have a mol2 or xyz input file molecule.write_pdb() # Parametrisation options: param_dict = {'antechamber': AnteChamber, 'xml': XML, 'openff': OpenFF} # If we are using xml we have to move it if molecule.parameter_engine == 'xml': copy(os.path.join(molecule.home, f'{molecule.name}.xml'), f'{molecule.name}.xml') # Perform the parametrisation param_dict[molecule.parameter_engine](molecule) append_to_log( f'Finishing parametrisation of molecule with {molecule.parameter_engine}' ) return molecule
def hessian(self, molecule): """Using the assigned bonds engine, calculate and extract the Hessian matrix.""" # TODO Because of QCEngine, nothing is being put into the hessian folder anymore # Need a way of writing QCEngine output to log file; still waiting on documentation ... append_to_log('Starting hessian calculation') molecule.get_bond_lengths(input_type='qm') # Check what engine to use if molecule.bonds_engine == 'g09': qm_engine = self.engine_dict[molecule.bonds_engine](molecule) # Use the checkpoint file as this has higher xyz precision try: copy( os.path.join(molecule.home, os.path.join('3_qm_optimise', 'lig.chk')), 'lig.chk') result = qm_engine.generate_input(input_type='qm', hessian=True, restart=True) except FileNotFoundError: append_to_log( 'qm_optimise checkpoint not found, optimising first to refine atomic coordinates' ) result = qm_engine.generate_input(input_type='qm', optimise=True, hessian=True) if result['success']: molecule.hessian = qm_engine.hessian() else: raise Exception( 'The hessian was not calculated check the log file.') else: qceng = QCEngine(molecule) molecule.hessian = qceng.call_qcengine('psi4', 'hessian', input_type='qm') append_to_log( f'Finishing Hessian calculation using {molecule.bonds_engine}') return molecule
def qm_optimise(self, molecule): """Optimise the molecule with or without geometric.""" # TODO this method's not always printing completion to log file. append_to_log('Starting qm_optimisation') qm_engine = self.engine_dict[molecule.bonds_engine](molecule) if molecule.geometric and molecule.bonds_engine == 'psi4': qceng = QCEngine(molecule) # See if the structure is there if not we did not optimise if molecule.coords['mm'].any(): result = qceng.call_qcengine('geometric', 'gradient', input_type='mm') else: result = qceng.call_qcengine('geometric', 'gradient', input_type='input') # Check if converged and get the geometry if result['success']: # Load all of the frames into the molecule's trajectory holder molecule.read_geometric_traj(result['trajectory']) # store the last frame as the qm optimised structure molecule.coords['qm'] = molecule.coords['traj'][-1] # Write out the trajectory file molecule.write_xyz(input_type='traj', name=f'{molecule.name}_opt') molecule.write_xyz(input_type='qm', name='opt') append_to_log( f'Finishing qm_optimisation of molecule{" using geometric" if molecule.geometric else ""}' ) return molecule else: # TODO catch the qcengine error here print(result) # catch the steps done so far raise OptimisationFailed("The optimisation did not converge") elif molecule.coords['mm'].any(): result = qm_engine.generate_input(input_type='mm', optimise=True) else: result = qm_engine.generate_input(input_type='input', optimise=True) # Check the exit status of the job; if failed restart the job up to 2 times restart_count = 1 while not result['success'] and restart_count < 3: append_to_log( f'{molecule.bonds_engine} optimisation failed with error {result["error"]}; restarting', msg_type='minor') # Now we should handle the errors that we have in the results # 1) If we have a file read error just start again if result['error'] == 'FileIO': result = qm_engine.generate_input(input_type='mm', optimise=True, restart=True) # 2) If we have a distance matrix error we should start from a different structure try the input elif result['error'] == 'Distance matrix' and restart_count == 1: result = qm_engine.generate_input(input_type='input', optimise=True) # 3) If we have already tried the starting structure generate a conformer and try again elif result['error'] == 'Distance matrix': molecule.write_pdb() rdkit = RDKit() molecule.coords['temp'] = rdkit.generate_conformers( f'{molecule.name}.pdb')[0] result = qm_engine.generate_input(input_type='temp', optimise=True) restart_count += 1 if not result['success']: raise OptimisationFailed( f"{molecule.bonds_engine} " f"optimisation did not converge after 3 restarts; last error {result['error']}" ) molecule.coords[ 'qm'], molecule.qm_energy = qm_engine.optimised_structure() molecule.write_xyz(input_type='qm', name='opt') append_to_log( f'Finishing qm_optimisation of molecule{" using geometric" if molecule.geometric else ""}' ) return molecule
def generate_input(self, input_type='input', optimise=False, hessian=False, density=False, energy=False, fchk=False, restart=False, execute=True): """ Converts to psi4 input format to be run in psi4 without using geometric. :param input_type: The coordinate set of the molecule to be used :param optimise: Optimise the molecule to the desired convergence critera with in the iteration limit :param hessian: Calculate the hessian matrix :param density: Calculate the electron density :param energy: Calculate the single point energy of the molecule :param fchk: Write out a gaussian style Fchk file :param restart: Restart the calculation from a log point :param execute: Run the desired Psi4 job :return: The completion status of the job True if successful False if not run or failed """ molecule = self.molecule.molecule[input_type] setters = '' tasks = '' # input.dat is the PSI4 input file. with open('input.dat', 'w+') as input_file: # opening tag is always writen input_file.write( f"memory {self.molecule.memory} GB\n\nmolecule {self.molecule.name} {{\n{self.molecule.charge} {self.molecule.multiplicity} \n" ) # molecule is always printed for atom in molecule: input_file.write( f' {atom[0]} {float(atom[1]): .10f} {float(atom[2]): .10f} {float(atom[3]): .10f} \n' ) input_file.write( f" units angstrom\n no_reorient\n}}\n\nset {{\n basis {self.molecule.basis}\n" ) if energy: append_to_log('Writing psi4 energy calculation input') tasks += f"\nenergy = energy('{self.molecule.theory}')" if optimise: append_to_log('Writing PSI4 optimisation input', 'minor') setters += f" g_convergence {self.molecule.convergence}\n GEOM_MAXITER {self.molecule.iterations}\n" tasks += f"\noptimize('{self.molecule.theory.lower()}')" if hessian: append_to_log('Writing PSI4 Hessian matrix calculation input', 'minor') setters += ' hessian_write on\n' tasks += f"\nenergy, wfn = frequency('{self.molecule.theory.lower()}', return_wfn=True)" tasks += '\nwfn.hessian().print_out()\n\n' if density: append_to_log('Writing PSI4 density calculation input', 'minor') setters += " cubeprop_tasks ['density']\n" overage = get_overage(self.molecule.name) setters += " CUBIC_GRID_OVERAGE [{0}, {0}, {0}]\n".format( overage) setters += " CUBIC_GRID_SPACING [0.13, 0.13, 0.13]\n" tasks += f"grad, wfn = gradient('{self.molecule.theory.lower()}', return_wfn=True)\ncubeprop(wfn)" if fchk: append_to_log('Writing PSI4 input file to generate fchk file') tasks += f"\ngrad, wfn = gradient('{self.molecule.theory.lower()}', return_wfn=True)" tasks += '\nfchk_writer = psi4.core.FCHKWriter(wfn)' tasks += f'\nfchk_writer.write("{self.molecule.name}_psi4.fchk")\n' # TODO If overage cannot be made to work, delete and just use Gaussian. # if self.molecule.solvent: # setters += ' pcm true\n pcm_scf_type total\n' # tasks += '\n\npcm = {' # tasks += '\n units = Angstrom\n Medium {\n SolverType = IEFPCM\n Solvent = Chloroform\n }' # tasks += '\n Cavity {\n RadiiSet = UFF\n Type = GePol\n Scaling = False\n Area = 0.3\n Mode = Implicit' # tasks += '\n }\n}' setters += '}\n' if not execute: setters += f'set_num_threads({self.molecule.threads})\n' input_file.write(setters) input_file.write(tasks) if execute: with open('log.txt', 'w+') as log: sp.run(f'psi4 input.dat -n {self.molecule.threads}', shell=True, stdout=log, stderr=log) # After running, check for normal termination return True if '*** Psi4 exiting successfully.' in open( 'output.dat', 'r').read() else False else: return False