Source code for custodian.qchem.new_jobs

# coding: utf-8

from __future__ import unicode_literals, division
import math

# New QChem job module


import os
import shutil
import copy
import subprocess
import numpy as np
from pymatgen.core import Molecule
from pymatgen.io.qchem_io.inputs import QCInput
from pymatgen.io.qchem_io.outputs import QCOutput
from custodian.custodian import Job
from pymatgen.analysis.molecule_structure_comparator import MoleculeStructureComparator

__author__ = "Samuel Blau, Brandon Woods, Shyam Dwaraknath"
__copyright__ = "Copyright 2018, The Materials Project"
__version__ = "0.1"
__maintainer__ = "Samuel Blau"
__email__ = "samblau1@gmail.com"
__status__ = "Alpha"
__date__ = "3/20/18"
__credits__ = "Xiaohui Qu"


[docs]class QCJob(Job): """ A basic QChem Job. """ def __init__(self, qchem_command, multimode="openmp", input_file="mol.qin", output_file="mol.qout", max_cores=32, qclog_file="mol.qclog", suffix="", scratch_dir="/dev/shm/qcscratch/", save_scratch=False, save_name="default_save_name"): """ Args: qchem_command (str): Command to run QChem. multimode (str): Parallelization scheme, either openmp or mpi. input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. max_cores (int): Maximum number of cores to parallelize over. Defaults to 32. qclog_file (str): Name of the file to redirect the standard output to. None means not to record the standard output. Defaults to None. suffix (str): String to append to the file in postprocess. scratch_dir (str): QCSCRATCH directory. Defaults to "/dev/shm/qcscratch/". save_scratch (bool): Whether to save scratch directory contents. Defaults to False. save_name (str): Name of the saved scratch directory. Defaults to to "default_save_name". """ self.qchem_command = qchem_command.split(" ") self.multimode = multimode self.input_file = input_file self.output_file = output_file self.max_cores = max_cores self.qclog_file = qclog_file self.suffix = suffix self.scratch_dir = scratch_dir self.save_scratch = save_scratch self.save_name = save_name @property def current_command(self): multimode_index = 0 if self.save_scratch: command = [ "-save", "", str(self.max_cores), self.input_file, self.output_file, self.save_name ] multimode_index = 1 else: command = [ "", str(self.max_cores), self.input_file, self.output_file ] if self.multimode == 'openmp': command[multimode_index] = "-nt" elif self.multimode == 'mpi': command[multimode_index] = "-np" else: print("ERROR: Multimode should only be set to openmp or mpi") command = self.qchem_command + command return command
[docs] def setup(self): os.putenv("QCSCRATCH", self.scratch_dir) if self.multimode == 'openmp': os.putenv('QCTHREADS', str(self.max_cores)) os.putenv('OMP_NUM_THREADS', str(self.max_cores))
[docs] def postprocess(self): if self.save_scratch: shutil.copytree( os.path.join(self.scratch_dir, self.save_name), os.path.join(os.path.dirname(self.input_file), self.save_name)) if self.suffix != "": shutil.move(self.input_file, self.input_file + self.suffix) shutil.move(self.output_file, self.output_file + self.suffix) shutil.move(self.qclog_file, self.qclog_file + self.suffix)
[docs] def run(self): """ Perform the actual QChem run. Returns: (subprocess.Popen) Used for monitoring. """ qclog = open(self.qclog_file, 'w') p = subprocess.Popen(self.current_command, stdout=qclog) return p
[docs] @classmethod def opt_with_frequency_flattener(cls, qchem_command, multimode="openmp", input_file="mol.qin", output_file="mol.qout", qclog_file="mol.qclog", max_iterations=10, max_molecule_perturb_scale=0.3, reversed_direction=False, ignore_connectivity=False, **QCJob_kwargs): """ Optimize a structure and calculate vibrational frequencies to check if the structure is in a true minima. If a frequency is negative, iteratively perturbe the geometry, optimize, and recalculate frequencies until all are positive, aka a true minima has been found. Args: qchem_command (str): Command to run QChem. multimode (str): Parallelization scheme, either openmp or mpi. input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file max_iterations (int): Number of perturbation -> optimization -> frequency iterations to perform. Defaults to 10. max_molecule_perturb_scale (float): The maximum scaled perturbation that can be applied to the molecule. Defaults to 0.3. reversed_direction (bool): Whether to reverse the direction of the vibrational frequency vectors. Defaults to False. ignore_connectivity (bool): Whether to ignore differences in connectivity introduced by structural perturbation. Defaults to False. **QCJob_kwargs: Passthrough kwargs to QCJob. See :class:`custodian.qchem.new_jobs.QCJob`. """ min_molecule_perturb_scale = 0.1 scale_grid = 10 perturb_scale_grid = ( max_molecule_perturb_scale - min_molecule_perturb_scale ) / scale_grid msc = MoleculeStructureComparator() if not os.path.exists(input_file): raise AssertionError('Input file must be present!') orig_opt_input = QCInput.from_file(input_file) orig_opt_rem = copy.deepcopy(orig_opt_input.rem) orig_freq_rem = copy.deepcopy(orig_opt_input.rem) orig_freq_rem["job_type"] = "freq" for ii in range(max_iterations): yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".opt_" + str(ii), **QCJob_kwargs)) opt_outdata = QCOutput(output_file + ".opt_" + str(ii)).data freq_QCInput = QCInput( molecule=opt_outdata.get("molecule_from_optimized_geometry"), rem=orig_freq_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent) freq_QCInput.write_file(input_file) yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_" + str(ii), **QCJob_kwargs)) outdata = QCOutput(output_file + ".freq_" + str(ii)).data errors = outdata.get("errors") if len(errors) != 0: raise AssertionError('No errors should be encountered while flattening frequencies!') if outdata.get('frequencies')[0] > 0.0: print("All frequencies positive!") break else: negative_freq_vecs = outdata.get("frequency_mode_vectors")[0] old_coords = outdata.get("initial_geometry") old_molecule = outdata.get("initial_molecule") structure_successfully_perturbed = False for molecule_perturb_scale in np.arange( max_molecule_perturb_scale, min_molecule_perturb_scale, -perturb_scale_grid): new_coords = perturb_coordinates( old_coords=old_coords, negative_freq_vecs=negative_freq_vecs, molecule_perturb_scale=molecule_perturb_scale, reversed_direction=reversed_direction) new_molecule = Molecule( species=outdata.get('species'), coords=new_coords, charge=outdata.get('charge'), spin_multiplicity=outdata.get('multiplicity')) if msc.are_equal(old_molecule, new_molecule) or ignore_connectivity: structure_successfully_perturbed = True break if not structure_successfully_perturbed: raise Exception( "Unable to perturb coordinates to remove negative frequency without changing the bonding structure" ) new_opt_QCInput = QCInput( molecule=new_molecule, rem=orig_opt_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent) new_opt_QCInput.write_file(input_file)
[docs]def perturb_coordinates(old_coords, negative_freq_vecs, molecule_perturb_scale, reversed_direction): max_dis = max( [math.sqrt(sum([x**2 for x in vec])) for vec in negative_freq_vecs]) scale = molecule_perturb_scale / max_dis normalized_vecs = [[x * scale for x in vec] for vec in negative_freq_vecs] direction = 1.0 if reversed_direction: direction = -1.0 return [[c + v * direction for c, v in zip(coord, vec)] for coord, vec in zip(old_coords, normalized_vecs)]