Skip to content

Commit a041423

Browse files
authored
Merge pull request #770 from samblau/qchem
Q-Chem 6 + Misc Updates
2 parents 17e7a7f + 30c1028 commit a041423

Some content is hidden

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

60 files changed

+183676
-64
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
export PATH=$HOME/miniconda3/bin:$PATH
1616
conda create --name test_env python=3.8
1717
source activate test_env
18-
conda install -c conda-forge openbabel pymatgen
18+
conda install -c conda-forge openbabel gcc_linux-64
1919
pip install -r requirements-ci.txt
2020
pip install .
2121
no_output_timeout: 1h

atomate/qchem/drones.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ def generate_doc(self, dir_name, qcinput_files, qcoutput_files, multirun):
233233
if d["output"]["job_type"] in ["freq", "frequency"]:
234234
d["output"]["frequencies"] = d_calc_final["frequencies"]
235235
# Note: for single-atom freq calcs, this key may not exist
236-
d["output"]["frequency_modes"] = d_calc_final.get("frequency_mode_vectors", [])
236+
d["output"]["frequency_modes"] = d_calc_final.get(
237+
"frequency_mode_vectors", []
238+
)
237239
d["output"]["enthalpy"] = d_calc_final["total_enthalpy"]
238240
d["output"]["entropy"] = d_calc_final["total_entropy"]
239241
if d["input"]["job_type"] in ["opt", "optimization", "ts"]:
@@ -275,12 +277,11 @@ def generate_doc(self, dir_name, qcinput_files, qcoutput_files, multirun):
275277
if d_calc_final["CDS_gradients"] is not None:
276278
d["output"]["CDS_gradients"] = d_calc_final["CDS_gradients"][0]
277279

278-
if d["output"]["job_type"] == "force":
279-
d["output"]["gradients"] = d_calc_final["gradients"][0]
280-
if d_calc_final["pcm_gradients"] is not None:
281-
d["output"]["pcm_gradients"] = d_calc_final["pcm_gradients"][0]
282-
if d_calc_final["CDS_gradients"] is not None:
283-
d["output"]["CDS_gradients"] = d_calc_final["CDS_gradients"][0]
280+
if d["output"]["job_type"] in ["force", "sp"]:
281+
d["output"]["dipoles"] = d_calc_final["dipoles"]
282+
if "gap_info" in d_calc_final:
283+
if d_calc_final["gap_info"] is not None:
284+
d["output"]["gap_info"] = d_calc_final["gap_info"]
284285

285286
opt_trajectory = []
286287
calcs = copy.deepcopy(d["calcs_reversed"])
@@ -451,7 +452,7 @@ def process_qchem_multirun(dir_name, input_files, output_files):
451452
qchem_input_file = os.path.join(dir_name, input_files.get(key))
452453
qchem_output_file = os.path.join(dir_name, output_files.get(key))
453454
multi_out = QCOutput.multiple_outputs_from_file(
454-
QCOutput, qchem_output_file, keep_sub_files=False
455+
filename=qchem_output_file, keep_sub_files=False
455456
)
456457
multi_in = QCInput.from_multi_jobs_file(qchem_input_file)
457458
for ii, out in enumerate(multi_out):

atomate/qchem/firetasks/parse_outputs.py

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import json
22
import os
3+
import shutil
4+
import struct
5+
from monty.io import zopen
36

47
from fireworks import FiretaskBase, FWAction, explicit_serialize
58
from fireworks.utilities.fw_serializers import DATETIME_HANDLER
@@ -50,6 +53,8 @@ class QChemToDb(FiretaskBase):
5053
"calc_loc",
5154
"input_file",
5255
"output_file",
56+
"parse_grad_file",
57+
"parse_hess_file",
5358
"additional_fields",
5459
"db_file",
5560
"fw_spec_field",
@@ -84,13 +89,123 @@ def run_task(self, fw_spec):
8489
multirun=multirun,
8590
)
8691

92+
logger.info("Drone finished!")
93+
94+
# parse the GRAD file, if desired and if it is present
95+
if self.get("parse_grad_file", False):
96+
grad_file = None
97+
if os.path.exists(os.path.join(calc_dir, "GRAD.gz")):
98+
grad_file = os.path.join(calc_dir, "GRAD.gz")
99+
elif os.path.exists(os.path.join(calc_dir, "GRAD")):
100+
grad_file = os.path.join(calc_dir, "GRAD")
101+
elif os.path.exists(os.path.join(calc_dir, "scratch/GRAD.gz")):
102+
grad_file = os.path.join(calc_dir, "scratch/GRAD.gz")
103+
elif os.path.exists(os.path.join(calc_dir, "scratch/GRAD")):
104+
grad_file = os.path.join(calc_dir, "scratch/GRAD")
105+
elif os.path.exists(os.path.join(calc_dir, "131.0.gz")):
106+
grad_file = os.path.join(calc_dir, "131.0.gz")
107+
elif os.path.exists(os.path.join(calc_dir, "131.0")):
108+
grad_file = os.path.join(calc_dir, "131.0")
109+
elif os.path.exists(os.path.join(calc_dir, "scratch/131.0.gz")):
110+
grad_file = os.path.join(calc_dir, "scratch/131.0.gz")
111+
elif os.path.exists(os.path.join(calc_dir, "scratch/131.0")):
112+
grad_file = os.path.join(calc_dir, "scratch/131.0")
113+
114+
if grad_file is None:
115+
task_doc["warnings"]["grad_file_missing"] = True
116+
else:
117+
logger.info("Parsing gradient file")
118+
grad = []
119+
if grad_file[-5:] == "131.0" or grad_file[-8:] == "131.0.gz":
120+
tmp_grad_data = []
121+
logger.info("Gradient file is binary")
122+
with zopen(grad_file, mode="rb") as file:
123+
binary = file.read()
124+
for ii in range(int(len(binary) / 8)):
125+
tmp_grad_data.append(
126+
struct.unpack("d", binary[ii * 8 : (ii + 1) * 8])[0]
127+
)
128+
grad = []
129+
for ii in range(int(len(tmp_grad_data) / 3)):
130+
grad.append(
131+
[
132+
float(tmp_grad_data[ii * 3]),
133+
float(tmp_grad_data[ii * 3 + 1]),
134+
float(tmp_grad_data[ii * 3 + 2]),
135+
]
136+
)
137+
else:
138+
logger.info("Gradient file is text")
139+
with zopen(grad_file, mode="rt", encoding="ISO-8859-1") as f:
140+
lines = f.readlines()
141+
for line in lines:
142+
split_line = line.split()
143+
if len(split_line) == 3:
144+
grad.append(
145+
[
146+
float(split_line[0]),
147+
float(split_line[1]),
148+
float(split_line[2]),
149+
]
150+
)
151+
task_doc["output"]["precise_gradients"] = grad
152+
logger.info("Done parsing gradient. Now removing scratch directory")
153+
if os.path.exists(os.path.join(calc_dir, "scratch")):
154+
shutil.rmtree(os.path.join(calc_dir, "scratch"))
155+
156+
# parse the HESS file(s), if desired and if one or multiple are present
157+
if self.get("parse_hess_file", False):
158+
hess_files = []
159+
for filename in os.listdir(calc_dir):
160+
if "HESS" in filename:
161+
hess_files.append(filename)
162+
elif filename == "scratch":
163+
for subfilename in os.listdir(os.path.join(calc_dir, "scratch")):
164+
if "HESS" in subfilename:
165+
hess_files.append("scratch/" + subfilename)
166+
elif subfilename == "132.0" or subfilename == "132.0.gz":
167+
hess_files.append("scratch/" + subfilename)
168+
169+
if len(hess_files) == 0:
170+
task_doc["warnings"]["hess_file_missing"] = True
171+
else:
172+
logger.info("Parsing Hessian file")
173+
hess_data = {}
174+
for hess_name in hess_files:
175+
if hess_name[-5:] == "132.0" or hess_name[-8:] == "132.0.gz":
176+
logger.info("Hessian file is binary")
177+
tmp_hess_data = []
178+
with zopen(
179+
os.path.join(calc_dir, hess_name), mode="rb"
180+
) as file:
181+
binary = file.read()
182+
for ii in range(int(len(binary) / 8)):
183+
tmp_hess_data.append(
184+
struct.unpack("d", binary[ii * 8 : (ii + 1) * 8])[0]
185+
)
186+
hess_data[hess_name] = tmp_hess_data
187+
else:
188+
logger.info("Hessian file is text")
189+
with zopen(
190+
os.path.join(calc_dir, hess_name),
191+
mode="rt",
192+
encoding="ISO-8859-1",
193+
) as f:
194+
hess_data[hess_name] = f.readlines()
195+
task_doc["output"]["hess_data"] = hess_data
196+
logger.info("Done parsing Hessian. Now removing scratch directory")
197+
if os.path.exists(os.path.join(calc_dir, "scratch")):
198+
shutil.rmtree(os.path.join(calc_dir, "scratch"))
199+
87200
# Check for additional keys to set based on the fw_spec
201+
logger.info("Updating fw_spec_field")
88202
if self.get("fw_spec_field"):
89203
task_doc.update(
90204
{self.get("fw_spec_field"): fw_spec.get(self.get("fw_spec_field"))}
91205
)
92206

93207
# Update fw_spec with final/optimized structure
208+
logger.info("Updating charges in spec")
94209
update_spec = {}
95210
if task_doc.get("output").get("optimized_molecule"):
96211
update_spec["prev_calc_molecule"] = task_doc["output"]["optimized_molecule"]
@@ -101,14 +216,17 @@ def run_task(self, fw_spec):
101216
update_spec["prev_calc_esp"] = task_doc["output"]["ESP"]
102217

103218
# get the database connection
219+
logger.info("Get database connection")
104220
db_file = env_chk(self.get("db_file"), fw_spec)
105221

106222
# db insertion or taskdoc dump
107223
if not db_file:
108224
with open(os.path.join(calc_dir, "task.json"), "w") as f:
109225
f.write(json.dumps(task_doc, default=DATETIME_HANDLER))
110226
else:
227+
logger.info("Connecting to QChemCalcDb")
111228
mmdb = QChemCalcDb.from_db_file(db_file, admin=True)
229+
logger.info("Starting task_doc insert")
112230
t_id = mmdb.insert(task_doc)
113231
logger.info(f"Finished parsing with task_id: {t_id}")
114232

@@ -195,12 +313,26 @@ def run_task(self, fw_spec):
195313
task_doc_clean["orig"]["molecule"]["spin_multiplicity"] = 1
196314
task_doc_clean["output"]["initial_molecule"]["charge"] = 1
197315
task_doc_clean["output"]["initial_molecule"]["spin_multiplicity"] = 1
198-
task_doc_clean["output"]["initial_molecule"]["sites"] = [{'name': 'H', 'species': [{'element': 'H', 'occu': 1}], 'xyz': [0.0, 0.0, 0.0], 'properties': {}}]
316+
task_doc_clean["output"]["initial_molecule"]["sites"] = [
317+
{
318+
"name": "H",
319+
"species": [{"element": "H", "occu": 1}],
320+
"xyz": [0.0, 0.0, 0.0],
321+
"properties": {},
322+
}
323+
]
199324
task_doc_clean["output"]["mulliken"] = [+1.000000]
200325
task_doc_clean["output"]["resp"] = [+1.000000]
201326
task_doc_clean["output"]["optimized_molecule"]["charge"] = 1
202327
task_doc_clean["output"]["optimized_molecule"]["spin_multiplicity"] = 1
203-
task_doc_clean["output"]["optimized_molecule"]["sites"] = [{'name': 'H', 'species': [{'element': 'H', 'occu': 1}], 'xyz': [0.0, 0.0, 0.0], 'properties': {}}]
328+
task_doc_clean["output"]["optimized_molecule"]["sites"] = [
329+
{
330+
"name": "H",
331+
"species": [{"element": "H", "occu": 1}],
332+
"xyz": [0.0, 0.0, 0.0],
333+
"properties": {},
334+
}
335+
]
204336
task_doc_clean["output"]["final_energy"] = (
205337
task_doc_2["output"]["final_energy"] - task_doc_1["output"]["final_energy"]
206338
)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
import shutil
3+
import unittest
4+
5+
from pymatgen.core import Molecule
6+
from pymatgen.io.qchem.inputs import QCInput
7+
8+
from atomate.qchem.firetasks.parse_outputs import QChemToDb
9+
from atomate.utils.testing import AtomateTest
10+
11+
from monty.serialization import loadfn
12+
13+
__author__ = "Samuel Blau"
14+
__email__ = "[email protected]"
15+
16+
module_dir = os.path.dirname(os.path.abspath(__file__))
17+
18+
19+
class TestParseOutputQChem_grads(AtomateTest):
20+
def setUp(self, lpad=False):
21+
super().setUp(lpad=False)
22+
23+
def tearDown(self):
24+
pass
25+
26+
def test_parse_grad_good(self):
27+
my_calc_dir = os.path.join(module_dir, "..", "..", "test_files","parse_grad_good")
28+
ft = QChemToDb(calc_dir=my_calc_dir, parse_grad_file=True)
29+
ft.run_task({})
30+
task_doc = loadfn(os.path.join(my_calc_dir,"task.json"))
31+
self.assertEqual(task_doc["output"]["final_energy"], -274.6893362188)
32+
self.assertEqual(len(task_doc["output"]["precise_gradients"]), 10)
33+
self.assertEqual(task_doc["output"]["precise_gradients"][0], [0.0090906486788787, 0.016150932052898, 0.0054568671405536])
34+
self.assertEqual(task_doc["output"]["precise_gradients"][-1], [0.0014495621906601, -0.0018570062958895, 0.0012478282193499])
35+
os.remove(os.path.join(my_calc_dir, "task.json"))
36+
37+
def test_parse_grad_131(self):
38+
my_calc_dir = os.path.join(module_dir, "..", "..", "test_files","tmqm_grad_pcm")
39+
ft = QChemToDb(calc_dir=my_calc_dir, parse_grad_file=True)
40+
ft.run_task({})
41+
task_doc = loadfn(os.path.join(my_calc_dir,"task.json"))
42+
self.assertEqual(task_doc["output"]["final_energy"], -2791.8404057999)
43+
self.assertEqual(len(task_doc["output"]["precise_gradients"]), 25)
44+
self.assertEqual(task_doc["output"]["precise_gradients"][0], [-2.7425178677332305e-05, 1.8017443772144412e-06, -2.3689773769176685e-06])
45+
self.assertEqual(task_doc["output"]["precise_gradients"][-1], [0.0028753270363098644, -0.000392640066359285, 0.004405091870637312])
46+
os.remove(os.path.join(my_calc_dir, "task.json"))
47+
48+
def test_parse_grad_bad(self):
49+
my_calc_dir = os.path.join(module_dir, "..", "..", "test_files","parse_grad_bad")
50+
ft = QChemToDb(calc_dir=my_calc_dir, parse_grad_file=True)
51+
ft.run_task({})
52+
task_doc = loadfn(os.path.join(my_calc_dir,"task.json"))
53+
self.assertEqual(task_doc["warnings"]["grad_file_missing"], True)
54+
os.remove(os.path.join(my_calc_dir, "task.json"))
55+
56+
57+
class TestParseOutputQChem_hess(AtomateTest):
58+
def setUp(self, lpad=False):
59+
os.makedirs(os.path.join(module_dir, "..", "..", "test_files", "freq_save_hess", "scratch"))
60+
shutil.copyfile(
61+
os.path.join(module_dir, "..", "..", "test_files", "freq_save_hess", "BUP_scratch", "132.0"),
62+
os.path.join(module_dir, "..", "..", "test_files", "freq_save_hess", "scratch", "132.0"),
63+
)
64+
shutil.copyfile(
65+
os.path.join(module_dir, "..", "..", "test_files", "freq_save_hess", "BUP_scratch", "HESS"),
66+
os.path.join(module_dir, "..", "..", "test_files", "freq_save_hess", "scratch", "HESS"),
67+
)
68+
super().setUp(lpad=False)
69+
70+
def test_parse_hess(self):
71+
my_calc_dir = os.path.join(module_dir, "..", "..", "test_files","freq_save_hess")
72+
ft = QChemToDb(calc_dir=my_calc_dir, parse_hess_file=True)
73+
ft.run_task({})
74+
task_doc = loadfn(os.path.join(my_calc_dir,"task.json"))
75+
self.assertEqual(task_doc["output"]["final_energy"], -151.3244603665)
76+
self.assertEqual(task_doc["output"]["hess_data"]["scratch/132.0"][0],0.12636293260949633)
77+
self.assertEqual(task_doc["output"]["hess_data"]["scratch/132.0"][-2],-0.2025032138024329)
78+
self.assertEqual(task_doc["output"]["hess_data"]["scratch/HESS"][-2], ' -0.175476533300377 -0.202503213802433 0.205623571433770 \n')
79+
os.remove(os.path.join(my_calc_dir, "task.json"))
80+
81+
def tearDown(self):
82+
pass
83+
84+
if __name__ == "__main__":
85+
unittest.main()

atomate/qchem/firetasks/tests/test_run_calc.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -543,10 +543,14 @@ def test_RunQChemFake(self):
543543
).data
544544
this_out = QCOutput("mol.qout").data
545545
for key in ref_out:
546-
try:
547-
self.assertEqual(ref_out[key], this_out[key])
548-
except ValueError:
549-
np.testing.assert_array_equal(ref_out[key], this_out[key])
546+
if key == "dipoles":
547+
self.assertEqual(ref_out[key]["total"], this_out[key]["total"])
548+
np.testing.assert_array_equal(ref_out[key]["dipole"], this_out[key]["dipole"])
549+
else:
550+
try:
551+
self.assertEqual(ref_out[key], this_out[key])
552+
except ValueError:
553+
np.testing.assert_array_equal(ref_out[key], this_out[key])
550554
for filename in os.listdir(
551555
os.path.join(module_dir, "..", "..", "test_files", "real_run")
552556
):

0 commit comments

Comments
 (0)