|
| 1 | +# coding: utf-8 |
| 2 | + |
| 3 | + |
| 4 | +# This module defines a Firetask that runs Critic2 to analyze a Q-Chem electron density. |
| 5 | + |
| 6 | + |
| 7 | +import shutil |
| 8 | +import os |
| 9 | +import subprocess |
| 10 | +import logging |
| 11 | +import warnings |
| 12 | + |
| 13 | +from pymatgen.io.qchem.inputs import QCInput |
| 14 | +from pymatgen.command_line.critic2_caller import Critic2Caller |
| 15 | +from monty.serialization import loadfn, dumpfn |
| 16 | + |
| 17 | +from custodian import Custodian |
| 18 | +from custodian.qchem.handlers import QChemErrorHandler |
| 19 | +from custodian.qchem.jobs import QCJob |
| 20 | + |
| 21 | +from monty.tempfile import ScratchDir |
| 22 | +from monty.shutil import compress_file, decompress_file |
| 23 | +from fireworks import explicit_serialize, FiretaskBase |
| 24 | + |
| 25 | +from atomate.utils.utils import env_chk, get_logger |
| 26 | +import numpy as np |
| 27 | + |
| 28 | +__author__ = "Samuel Blau" |
| 29 | +__copyright__ = "Copyright 2019, The Materials Project" |
| 30 | +__version__ = "0.1" |
| 31 | +__maintainer__ = "Samuel Blau" |
| 32 | + |
| 33 | +__status__ = "Alpha" |
| 34 | +__date__ = "11/20/19" |
| 35 | + |
| 36 | + |
| 37 | +logger = get_logger(__name__) |
| 38 | + |
| 39 | +@explicit_serialize |
| 40 | +class RunCritic2(FiretaskBase): |
| 41 | + """ |
| 42 | + Run the Critic2 package on an electron density cube file produced by a Q-Chem single point calculation |
| 43 | + to generate CP and YT files for electron density critical points analysis. |
| 44 | +
|
| 45 | + Required params: |
| 46 | + molecule (Molecule): Molecule object of the molecule whose electron density is being analyzed |
| 47 | + Note that if prev_calc_molecule is set in the firework spec it will override |
| 48 | + the molecule required param. |
| 49 | + cube_file (str): Name of the cube file being analyzed |
| 50 | +
|
| 51 | + """ |
| 52 | + |
| 53 | + required_params = ["molecule", "cube_file"] |
| 54 | + |
| 55 | + def run_task(self, fw_spec): |
| 56 | + if fw_spec.get("prev_calc_molecule"): |
| 57 | + molecule = fw_spec.get("prev_calc_molecule") |
| 58 | + else: |
| 59 | + molecule = self.get("molecule") |
| 60 | + if molecule == None: |
| 61 | + raise ValueError("No molecule passed and no prev_calc_molecule found in spec! Exiting...") |
| 62 | + |
| 63 | + compress_at_end = False |
| 64 | + |
| 65 | + cube = self.get("cube_file") |
| 66 | + |
| 67 | + if cube[-3:] == ".gz": |
| 68 | + compress_at_end = True |
| 69 | + decompress_file(cube) |
| 70 | + cube = cube[:-3] |
| 71 | + |
| 72 | + input_script = ["molecule " + cube] |
| 73 | + input_script += ["load " + cube] |
| 74 | + input_script += ["auto"] |
| 75 | + input_script += ["CPREPORT cpreport.json"] |
| 76 | + input_script += ["YT JSON yt.json"] |
| 77 | + input_script += ["end"] |
| 78 | + input_script += [""] |
| 79 | + input_script = "\n".join(input_script) |
| 80 | + |
| 81 | + caller = Critic2Caller(input_script) |
| 82 | + |
| 83 | + if compress_at_end: |
| 84 | + compress_file(cube) |
| 85 | + |
| 86 | + |
| 87 | +@explicit_serialize |
| 88 | +class ProcessCritic2(FiretaskBase): |
| 89 | + """ |
| 90 | + Process the CP and YT json outputs from a Critic2 execution |
| 91 | +
|
| 92 | + Required params: |
| 93 | + molecule (Molecule): Molecule object of the molecule whose electron density is being analyzed |
| 94 | + Note that if prev_calc_molecule is set in the firework spec it will override |
| 95 | + the molecule required param. |
| 96 | + """ |
| 97 | + |
| 98 | + required_params = ["molecule"] |
| 99 | + |
| 100 | + def run_task(self, fw_spec): |
| 101 | + if fw_spec.get("prev_calc_molecule"): |
| 102 | + molecule = fw_spec.get("prev_calc_molecule") |
| 103 | + else: |
| 104 | + molecule = self.get("molecule") |
| 105 | + if molecule == None: |
| 106 | + raise ValueError("No molecule passed and no prev_calc_molecule found in spec! Exiting...") |
| 107 | + |
| 108 | + cp_loaded = loadfn("cpreport.json") |
| 109 | + bohr_to_ang = 0.529177249 |
| 110 | + |
| 111 | + species = {} |
| 112 | + for specie in cp_loaded["structure"]["species"]: |
| 113 | + if specie["name"][1] == "_": |
| 114 | + species[specie["id"]] = specie["name"][0] |
| 115 | + else: |
| 116 | + species[specie["id"]] = specie["name"] |
| 117 | + |
| 118 | + atoms = [] |
| 119 | + centering_vector = cp_loaded["structure"]["molecule_centering_vector"] |
| 120 | + for ii,atom in enumerate(cp_loaded["structure"]["nonequivalent_atoms"]): |
| 121 | + specie = species[atom["species"]] |
| 122 | + atoms.append(specie) |
| 123 | + tmp = atom["cartesian_coordinates"] |
| 124 | + coords = [] |
| 125 | + for jj,val in enumerate(tmp): |
| 126 | + coords.append((val+centering_vector[jj])*bohr_to_ang) |
| 127 | + if str(molecule[ii].specie) != specie: |
| 128 | + raise RuntimeError("Atom ordering different!") |
| 129 | + if molecule[ii].distance_from_point(coords) > 1*10**-5: |
| 130 | + raise RuntimeError("Atom position "+str(ii)+" inconsistent!") |
| 131 | + |
| 132 | + if ( |
| 133 | + cp_loaded["critical_points"]["number_of_nonequivalent_cps"] != |
| 134 | + cp_loaded["critical_points"]["number_of_cell_cps"] |
| 135 | + ): |
| 136 | + raise ValueError("ERROR: number_of_nonequivalent_cps should always equal number_of_cell_cps!") |
| 137 | + |
| 138 | + bond_dict = {} |
| 139 | + for cp in cp_loaded["critical_points"]["nonequivalent_cps"]: |
| 140 | + if cp["rank"] == 3 and cp["signature"] == -1: |
| 141 | + bond_dict[cp["id"]] = {"field":cp["field"]} |
| 142 | + |
| 143 | + for cp in cp_loaded["critical_points"]["cell_cps"]: |
| 144 | + if cp["id"] in bond_dict: |
| 145 | + # Check if any bonds include fictitious atoms |
| 146 | + bad_bond = False |
| 147 | + for entry in cp["attractors"]: |
| 148 | + if int(entry["cell_id"])-1 >= len(atoms): |
| 149 | + bad_bond = True |
| 150 | + # If so, remove them from the bond_dict |
| 151 | + if bad_bond: |
| 152 | + bond_dict.pop(cp["id"]) |
| 153 | + else: |
| 154 | + bond_dict[cp["id"]]["atom_ids"] = [entry["cell_id"] for entry in cp["attractors"]] |
| 155 | + bond_dict[cp["id"]]["atoms"] = [atoms[int(entry["cell_id"])-1] for entry in cp["attractors"]] |
| 156 | + bond_dict[cp["id"]]["distance"] = cp["attractors"][0]["distance"]*bohr_to_ang+cp["attractors"][1]["distance"]*bohr_to_ang |
| 157 | + dumpfn(bond_dict,"bonding.json") |
| 158 | + |
| 159 | + bonds = [] |
| 160 | + for cpid in bond_dict: |
| 161 | + # identify and throw out fictitious bonds |
| 162 | + # NOTE: this should be re-examined and refined in the future |
| 163 | + if bond_dict[cpid]["atoms"] == ["Li","C"] or bond_dict[cpid]["atoms"] == ["C","Li"]: |
| 164 | + if bond_dict[cpid]["field"] > 0.012 and bond_dict[cpid]["distance"] < 2.5: |
| 165 | + bonds.append([int(entry)-1 for entry in bond_dict[cpid]["atom_ids"]]) |
| 166 | + elif bond_dict[cpid]["field"] > 0.02 and bond_dict[cpid]["distance"] < 2.5: |
| 167 | + bonds.append([int(entry)-1 for entry in bond_dict[cpid]["atom_ids"]]) |
| 168 | + |
| 169 | + yt = loadfn("yt.json") |
| 170 | + charges = [] |
| 171 | + for site in yt["integration"]["attractors"]: |
| 172 | + charges.append(site["atomic_number"]-site["integrals"][0]) |
| 173 | + |
| 174 | + processed_dict = {} |
| 175 | + processed_dict["bonds"] = bonds |
| 176 | + processed_dict["charges"] = charges |
| 177 | + dumpfn(processed_dict,"processed_critic2.json") |
0 commit comments