def hessian(self): """Extract the Hessian matrix from the Gaussian fchk file.""" # Make the fchk file first with open("formchck.log", "w+") as formlog: sp.run("formchk lig.chk lig.fchk", shell=True, stdout=formlog, stderr=formlog) with open("lig.fchk", "r") as fchk: lines = fchk.readlines() # Improperly formatted Hessian (converted to square numpy array later) hessian_list = [] start, end = None, None for count, line in enumerate(lines): if line.startswith("Cartesian Force Constants"): start = count + 1 if line.startswith("Nonadiabatic coupling"): if end is None: end = count if line.startswith("Dipole Moment"): if end is None: end = count if not start and end: raise EOFError( "Cannot locate Hessian matrix in lig.fchk file.") conversion = constants.HA_TO_KCAL_P_MOL / (constants.BOHR_TO_ANGS** 2) for line in lines[start:end]: # Extend the list with the converted floats from the file, splitting on spaces and removing '\n' tags. hessian_list.extend([ float(num) * conversion for num in line.strip("\n").split() ]) hess_size = 3 * len(self.molecule.atoms) hessian = np.zeros((hess_size, hess_size)) # Rewrite Hessian to full, symmetric 3N * 3N matrix rather than list with just the non-repeated values. m = 0 for i in range(hess_size): for j in range(i + 1): hessian[i, j] = hessian_list[m] hessian[j, i] = hessian_list[m] m += 1 check_symmetry(hessian) return hessian
def hessian(self): """ Parses the Hessian from the output.dat file (from psi4) into a numpy array; performs check to ensure it is symmetric; has some basic error handling for if the file is missing data etc. """ hess_size = 3 * len(self.molecule.atoms) # output.dat is the psi4 output file. with open('output.dat', 'r') as file: lines = file.readlines() for count, line in enumerate(lines): if '## Hessian' in line or '## New Matrix (Symmetry' in line: # Set the start of the hessian to the row of the first value. hess_start = count + 5 break else: raise EOFError('Cannot locate Hessian matrix in output.dat file.') # Check if the hessian continues over onto more lines (i.e. if hess_size is not divisible by 5) extra = 0 if hess_size % 5 == 0 else 1 # hess_length: # of cols * length of each col # + # of cols - 1 * #blank lines per row of hess_vals # + # blank lines per row of hess_vals if the hess_size continues over onto more lines. hess_length = (hess_size // 5) * hess_size + (hess_size // 5 - 1) * 3 + extra * (3 + hess_size) hess_end = hess_start + hess_length hess_vals = [] for file_line in lines[hess_start:hess_end]: # Compile lists of the 5 Hessian floats for each row. # Number of floats in last row may be less than 5. # Only the actual floats are added, not the separating numbers. row_vals = [float(val) for val in file_line.split() if len(val) > 5] hess_vals.append(row_vals) # Remove blank list entries hess_vals = [elem for elem in hess_vals if elem] reshaped = [] # Convert from list of (lists, length 5) to 2d array of size hess_size x hess_size for old_row in range(hess_size): new_row = [] for col_block in range(hess_size // 5 + extra): new_row += hess_vals[old_row + col_block * hess_size] reshaped.append(new_row) hess_matrix = np.array(reshaped) # Cache the unit conversion. conversion = constants.HA_TO_KCAL_P_MOL / (constants.BOHR_TO_ANGS ** 2) # Element-wise multiplication hess_matrix *= conversion check_symmetry(hess_matrix) return hess_matrix
def call_qcengine(self, engine, driver, input_type): """ Using the created schema, run a particular engine, specifying the driver (job type). e.g. engine: geo, driver: energies. :param engine: The engine to be used psi4 geometric :param driver: The calculation type to be done e.g. energy, gradient, hessian, properties :param input_type: The part of the molecule object that should be used when making the schema :return: The required driver information """ mol = self.generate_qschema(input_type=input_type) # Call psi4 for energy, gradient, hessian or property calculations if engine == 'psi4': psi4_task = qcel.models.ResultInput( molecule=mol, driver=driver, model={ 'method': self.molecule.theory, 'basis': self.molecule.basis }, keywords={'scf_type': 'df'}, ) ret = qcng.compute(psi4_task, 'psi4', local_options={ 'memory': self.molecule.memory, 'ncores': self.molecule.threads }) if driver == 'hessian': hess_size = 3 * len(self.molecule.atoms) conversion = constants.HA_TO_KCAL_P_MOL / ( constants.BOHR_TO_ANGS**2) hessian = np.reshape(ret.return_result, (hess_size, hess_size)) * conversion check_symmetry(hessian) return hessian else: return ret.return_result # Call geometric with psi4 to optimise a molecule elif engine == 'geometric': geo_task = { 'schema_name': 'qcschema_optimization_input', 'schema_version': 1, 'keywords': { 'coordsys': 'tric', 'maxiter': self.molecule.iterations, 'program': 'psi4', 'convergence_set': self.molecule.convergence, }, 'input_specification': { 'schema_name': 'qcschema_input', 'schema_version': 1, 'driver': 'gradient', 'model': { 'method': self.molecule.theory, 'basis': self.molecule.basis }, 'keywords': {}, }, 'initial_molecule': mol, } return qcng.compute_procedure(geo_task, 'geometric', return_dict=True, local_options={ 'memory': self.molecule.memory, 'ncores': self.molecule.threads }) else: raise KeyError( 'Invalid engine type provided. Please use "geo" or "psi4".')
def call_qcengine(self, engine, driver): """ Using the created schema, run a particular engine, specifying the driver (job type). e.g. engine: geo, driver: energies. :param engine: The engine to be used psi4 geometric :param driver: The calculation type to be done e.g. energy, gradient, hessian, properties :return: The required driver information """ mol = self.generate_qschema() options = {"memory": self.molecule.memory, "ncores": self.molecule.threads} # Call psi4 for energy, gradient, hessian or property calculations if engine == "psi4": psi4_task = qcel.models.AtomicInput( molecule=mol, driver=driver, model={"method": self.molecule.theory, "basis": self.molecule.basis}, keywords={"scf_type": "df"}, ) ret = qcng.compute(psi4_task, "psi4", local_options=options) if driver == "hessian": hess_size = 3 * len(self.molecule.atoms) conversion = constants.HA_TO_KCAL_P_MOL / (constants.BOHR_TO_ANGS ** 2) hessian = ( np.reshape(ret.return_result, (hess_size, hess_size)) * conversion ) check_symmetry(hessian) return hessian else: return ret.return_result # Call geometric with psi4 to optimise a molecule elif engine == "geometric": geo_task = { "schema_name": "qcschema_optimization_input", "schema_version": 1, "keywords": { "coordsys": "dlc", "maxiter": self.molecule.iterations, "program": "psi4", "convergence_set": self.molecule.convergence, }, "input_specification": { "schema_name": "qcschema_input", "schema_version": 1, "driver": "gradient", "model": { "method": self.molecule.theory, "basis": self.molecule.basis, }, "keywords": {}, }, "initial_molecule": mol, } return qcng.compute_procedure( geo_task, "geometric", return_dict=True, local_options=options ) elif engine == "torchani": ani_task = qcel.models.AtomicInput( molecule=mol, driver=driver, model={"method": "ANI1x", "basis": None}, keywords={"scf_type": "df"}, ) return qcng.compute( ani_task, "torchani", local_options=options ).return_result else: raise KeyError( 'Invalid engine type provided. Please use "geometric", "psi4" or "torchani".' )