Skip to content

Commit 54790cf

Browse files
authored
Merge pull request #598 from samblau/qchem
Q-Chem electron density critical point analysis via Critic2
2 parents f2e8e5e + 09e4fcd commit 54790cf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+22640
-9
lines changed

atomate/qchem/drones.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,23 @@ def post_process(dir_name, d):
371371
if len(filenames) >= 1:
372372
with zopen(filenames[0], "rt") as f:
373373
d["custom_smd"] = f.readlines()[0]
374+
filenames = glob.glob(os.path.join(fullpath, "processed_critic2.json*"))
375+
if len(filenames) >= 1:
376+
with zopen(filenames[0], "rt") as f:
377+
d["critic2"] = {}
378+
d["critic2"]["processed"] = json.load(f)
379+
filenames = glob.glob(os.path.join(fullpath, "cpreport.json*"))
380+
if len(filenames) >= 1:
381+
with zopen(filenames[0], "rt") as f:
382+
d["critic2"]["CP"] = json.load(f)
383+
filenames = glob.glob(os.path.join(fullpath, "yt.json*"))
384+
if len(filenames) >= 1:
385+
with zopen(filenames[0], "rt") as f:
386+
d["critic2"]["YT"] = json.load(f)
387+
filenames = glob.glob(os.path.join(fullpath, "bonding.json*"))
388+
if len(filenames) >= 1:
389+
with zopen(filenames[0], "rt") as f:
390+
d["critic2"]["bonding"] = json.load(f)
374391

375392
def validate_doc(self, d):
376393
"""

atomate/qchem/firetasks/critic2.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
__email__ = "[email protected]"
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")
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# coding: utf-8
2+
3+
import json
4+
import os
5+
import unittest
6+
import shutil
7+
from monty.os.path import which
8+
from atomate.qchem.firetasks.critic2 import RunCritic2, ProcessCritic2
9+
from atomate.utils.testing import AtomateTest
10+
from custodian.qchem.handlers import QChemErrorHandler
11+
from custodian.qchem.jobs import QCJob
12+
from pymatgen.io.qchem.outputs import QCOutput
13+
import numpy as np
14+
15+
__author__ = "Samuel Blau"
16+
__email__ = "[email protected]"
17+
18+
module_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)))
19+
20+
@unittest.skipIf(not which("critic2"), "critic2 executable not present")
21+
class TestRunCritic2(AtomateTest):
22+
def setUp(self, lpad=False):
23+
os.chdir(os.path.join(module_dir, "..", "..", "test_files",
24+
"critic_test_files", "small_critic_example"))
25+
out_file = "mol.qout"
26+
qc_out = QCOutput(filename=out_file)
27+
self.mol = qc_out.data["initial_molecule"]
28+
self.cube_file = "dens.0.cube"
29+
super(TestRunCritic2, self).setUp(lpad=False)
30+
31+
def tearDown(self):
32+
os.remove("cpreport.json")
33+
os.remove("yt.json")
34+
35+
def test_RunCritic2(self):
36+
os.chdir(os.path.join(module_dir, "..", "..", "test_files",
37+
"critic_test_files", "small_critic_example"))
38+
firetask = RunCritic2(
39+
molecule=self.mol,
40+
cube_file="dens.0.cube.gz")
41+
firetask.run_task(fw_spec={})
42+
with open("cpreport_correct.json") as f:
43+
cpreport_reference = json.load(f)
44+
with open("yt_correct.json") as f:
45+
yt_reference = json.load(f)
46+
with open("cpreport.json") as f:
47+
cpreport = json.load(f)
48+
with open("yt.json") as f:
49+
yt = json.load(f)
50+
# Context for below - reference files were built before units were added
51+
# to Critic2, and we avoid testing the actual critical points because they
52+
# can change order between runs. But comparing everything else is sufficient.
53+
for key in cpreport:
54+
if key in ["structure", "field"]:
55+
self.assertEqual(cpreport_reference[key],cpreport[key])
56+
for key in yt:
57+
if key != "units":
58+
self.assertEqual(yt_reference[key],yt[key])
59+
60+
61+
class TestProcessCritic2(AtomateTest):
62+
def setUp(self, lpad=False):
63+
os.chdir(os.path.join(module_dir, "..", "..", "test_files",
64+
"critic_test_files", "critic_example"))
65+
out_file = "mol.qout"
66+
qc_out = QCOutput(filename=out_file)
67+
self.mol = qc_out.data["initial_molecule"]
68+
self.cube_file = "dens.0.cube.gz"
69+
shutil.move("bonding.json","bonding_correct.json")
70+
shutil.move("processed_critic2.json","processed_correct.json")
71+
super(TestProcessCritic2, self).setUp(lpad=False)
72+
73+
def tearDown(self):
74+
os.remove("bonding.json")
75+
shutil.move("bonding_correct.json","bonding.json")
76+
os.remove("processed_critic2.json")
77+
shutil.move("processed_correct.json","processed_critic2.json")
78+
79+
def test_ProcessCritic2(self):
80+
os.chdir(os.path.join(module_dir, "..", "..", "test_files",
81+
"critic_test_files", "critic_example"))
82+
firetask = ProcessCritic2(
83+
molecule=self.mol)
84+
firetask.run_task(fw_spec={})
85+
with open("bonding_correct.json") as f:
86+
reference = json.load(f)
87+
with open("bonding.json") as f:
88+
just_built = json.load(f)
89+
self.assertEqual(reference,just_built)
90+
with open("processed_correct.json") as f:
91+
reference = json.load(f)
92+
with open("processed_critic2.json") as f:
93+
just_built = json.load(f)
94+
self.assertEqual(reference,just_built)
95+
96+
97+
if __name__ == "__main__":
98+
unittest.main()

0 commit comments

Comments
 (0)