Skip to content

Commit 7cebcb8

Browse files
committed
2 parents 0425db5 + 40536bf commit 7cebcb8

File tree

5 files changed

+299
-8
lines changed

5 files changed

+299
-8
lines changed

atomate/qchem/firetasks/parse_outputs.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,101 @@ def run_task(self, fw_spec):
116116
stored_data={"task_id": task_doc.get("task_id", None)},
117117
update_spec=update_spec,
118118
)
119+
120+
121+
@explicit_serialize
122+
class ProtCalcToDb(FiretaskBase):
123+
"""
124+
Enter a QChem run of a proton into the database. Uses current directory unless you
125+
specify calc_dir or calc_loc.
126+
127+
Optional params:
128+
calc_dir (str): path to dir (on current filesystem) that contains QChem
129+
input and output files for both the H0 and H2+. Default: use current working directory.
130+
calc_loc (str OR bool): if True will set most recent calc_loc. If str
131+
search for the most recent calc_loc with the matching name
132+
input_file_H0 (str): name of the QChem input file for H0 calculation
133+
output_file_H0 (str): name of the QChem output file for H0 calculation
134+
input_file_H2 (str): name of the QChem input file for H2 calculation
135+
output_file_H2 (str): name of the QChem output file for H2 calculation
136+
additional_fields (dict): dict of additional fields to add
137+
db_file (str): path to file containing the database credentials.
138+
Supports env_chk. Default: write data to JSON file.
139+
runs (list): Series of file suffixes that the Drone should look for
140+
when parsing output.
141+
"""
142+
143+
optional_params = [
144+
"calc_dir",
145+
"calc_loc",
146+
"input_file_H0",
147+
"output_file_H0",
148+
"input_file_H2",
149+
"output_file_H2",
150+
"additional_fields",
151+
"db_file",
152+
"runs",
153+
]
154+
155+
def run_task(self, fw_spec):
156+
# get the directory that contains the QChem dir to parse
157+
calc_dir = os.getcwd()
158+
if "calc_dir" in self:
159+
calc_dir = self["calc_dir"]
160+
elif self.get("calc_loc"):
161+
calc_dir = get_calc_loc(self["calc_loc"], fw_spec["calc_locs"])["path"]
162+
input_file_H0 = self.get("input_file_H0", "H0.qin")
163+
output_file_H0 = self.get("output_file_H0", "H0.qout")
164+
input_file_H2 = self.get("input_file_H2", "H2_plus.qin")
165+
output_file_H2 = self.get("output_file_H2", "H2_plus.qout")
166+
runs = self.get("runs", None)
167+
168+
# parse the QChem directory
169+
logger.info(f"PARSING DIRECTORY: {calc_dir}")
170+
171+
additional_fields = self.get("additional_fields", {})
172+
173+
drone = QChemDrone(runs=runs, additional_fields=additional_fields)
174+
175+
# assimilate (i.e., parse)
176+
task_doc_1 = drone.assimilate(
177+
path=calc_dir,
178+
input_file=input_file_H0,
179+
output_file=output_file_H0,
180+
multirun=False,
181+
)
182+
183+
task_doc_2 = drone.assimilate(
184+
path=calc_dir,
185+
input_file=input_file_H2,
186+
output_file=output_file_H2,
187+
multirun=False,
188+
)
189+
190+
task_doc_clean = task_doc_1
191+
task_doc_clean["calcs_reversed"].append(task_doc_2["calcs_reversed"][0])
192+
task_doc_clean["input"]["initial_molecule"]["charge"] = 1
193+
task_doc_clean["input"]["initial_molecule"]["spin_multiplicity"] = 1
194+
task_doc_clean["orig"]["molecule"]["charge"] = 1
195+
task_doc_clean["orig"]["molecule"]["spin_multiplicity"] = 1
196+
task_doc_clean["output"]["initial_molecule"]["charge"] = 1
197+
task_doc_clean["output"]["initial_molecule"]["spin_multiplicity"] = 1
198+
task_doc_clean["output"]["final_energy"] = (
199+
task_doc_2["output"]["final_energy"] - task_doc_1["output"]["final_energy"]
200+
)
201+
202+
# get the database connection
203+
db_file = env_chk(self.get("db_file"), fw_spec)
204+
205+
# db insertion or taskdoc dump
206+
if not db_file:
207+
with open(os.path.join(calc_dir, "task.json"), "w") as f:
208+
f.write(json.dumps(task_doc_clean, default=DATETIME_HANDLER))
209+
else:
210+
mmdb = QChemCalcDb.from_db_file(db_file, admin=True)
211+
t_id = mmdb.insert(task_doc_clean)
212+
logger.info(f"Finished parsing with task_id: {t_id}")
213+
214+
return FWAction(
215+
stored_data={"task_id": task_doc_clean.get("task_id", None)},
216+
)

atomate/qchem/firetasks/run_calc.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ def run_task(self, fw_spec):
144144
freq_before_opt = self.get("freq_before_opt", False)
145145

146146
handler_groups = {
147-
"default": [QChemErrorHandler(input_file=input_file, output_file=output_file)],
147+
"default": [
148+
QChemErrorHandler(input_file=input_file, output_file=output_file)
149+
],
148150
"no_handler": [],
149151
}
150152

@@ -206,7 +208,9 @@ def run_task(self, fw_spec):
206208
# construct handlers
207209
handlers = handler_groups[self.get("handler_group", "default")]
208210

209-
c = Custodian(handlers, jobs, max_errors=max_errors, gzipped_output=gzipped_output)
211+
c = Custodian(
212+
handlers, jobs, max_errors=max_errors, gzipped_output=gzipped_output
213+
)
210214

211215
c.run()
212216

@@ -248,7 +252,9 @@ def _verify_inputs(self):
248252
ref_qin = QCInput.from_file(os.path.join(self["ref_dir"], input_file))
249253

250254
np.testing.assert_equal(ref_qin.molecule.species, user_qin.molecule.species)
251-
np.testing.assert_allclose(ref_qin.molecule.cart_coords, user_qin.molecule.cart_coords, atol=0.0001)
255+
np.testing.assert_allclose(
256+
ref_qin.molecule.cart_coords, user_qin.molecule.cart_coords, atol=0.0001
257+
)
252258
for key in ref_qin.rem:
253259
if user_qin.rem.get(key) != ref_qin.rem.get(key):
254260
raise ValueError(f"Rem key {key} is inconsistent!")

atomate/qchem/fireworks/core.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from itertools import chain
66

77
from fireworks import Firework
8+
from pymatgen.core.sites import Site
9+
from pymatgen.core.structure import Molecule
810

911
from atomate.qchem.firetasks.critic2 import ProcessCritic2, RunCritic2
1012
from atomate.qchem.firetasks.fragmenter import FragmentMolecule
1113
from atomate.qchem.firetasks.geo_transformations import PerturbGeometry
12-
from atomate.qchem.firetasks.parse_outputs import QChemToDb
14+
from atomate.qchem.firetasks.parse_outputs import ProtCalcToDb, QChemToDb
1315
from atomate.qchem.firetasks.run_calc import RunQChemCustodian
1416
from atomate.qchem.firetasks.write_inputs import WriteInputFromIOSet
1517
from pymatgen.core.structure import Molecule
@@ -210,6 +212,121 @@ def __init__(
210212
super().__init__(t, parents=parents, name=name, **kwargs)
211213

212214

215+
class ProtonEnergyFW(Firework):
216+
def __init__(
217+
self,
218+
name="proton electronic energy",
219+
qchem_cmd=">>qchem_cmd<<",
220+
multimode=">>multimode<<",
221+
max_cores=">>max_cores<<",
222+
qchem_input_params=None,
223+
db_file=None,
224+
parents=None,
225+
max_errors=5,
226+
**kwargs
227+
):
228+
"""
229+
For this custom Firework the electronic energy of a proton in a specific solvent environment is approximated.
230+
Since a proton has 0 electrons,running a QChem job would yield an error. The energy can be approximated by
231+
calculating the electronic energy of a proton and a hydrogen atom at infinite separation and then subtracting
232+
the electronic energy of a hydrogen atom. This Firework combines these two calculations and adds a task doc to the
233+
DB with the separate calculation details and the effective energy after subtraction.
234+
235+
Args:
236+
name (str): Name for the Firework.
237+
qchem_cmd (str): Command to run QChem. Supports env_chk.
238+
multimode (str): Parallelization scheme, either openmp or mpi. Supports env_chk.
239+
max_cores (int): Maximum number of cores to parallelize over. Supports env_chk.
240+
qchem_input_params (dict): Specify kwargs for instantiating the input set parameters
241+
calculating the electronic energy of the proton in a specific solvent environment.
242+
The energy of the proton will be effectively zero in vacuum. Use either pcm_dieletric
243+
or some smd_solvent.Basic uses would be to modify the default inputs of the set,
244+
such as dft_rung, basis_set, pcm_dielectric, scf_algorithm, or max_scf_cycles.
245+
See pymatgen/io/qchem/sets.py for default values of all input parameters. For
246+
instance, if a user wanted to use a more advanced DFT functional, include a pcm
247+
with a dielectric of 30, and use a larger basis, the user would set
248+
qchem_input_params = {"dft_rung": 5, "pcm_dielectric": 30, "basis_set":
249+
"6-311++g**"}. However, more advanced customization of the input is also
250+
possible through the overwrite_inputs key which allows the user to directly
251+
modify the rem, pcm, smd, and solvent dictionaries that QChemDictSet passes to
252+
inputs.py to print an actual input file. For instance, if a user wanted to set
253+
the sym_ignore flag in the rem section of the input file to true, then they
254+
would set qchem_input_params = {"overwrite_inputs": "rem": {"sym_ignore":
255+
"true"}}. Of course, overwrite_inputs could be used in conjunction with more
256+
typical modifications, as seen in the test_double_FF_opt workflow test.
257+
db_file (str): Path to file specifying db credentials to place output parsing.
258+
parents ([Firework]): Parents of this particular Firework.
259+
**kwargs: Other kwargs that are passed to Firework.__init__.
260+
"""
261+
262+
qchem_input_params = qchem_input_params or {}
263+
264+
H_site = Site("H", [0.0, 0.0, 0.0])
265+
H_site_inf = Site("H", [100000.0, 0.0, 0.0])
266+
H0_atom = Molecule.from_sites([H_site])
267+
H2_plus_mol = Molecule.from_sites([H_site, H_site_inf])
268+
H0_atom.set_charge_and_spin(0, 2)
269+
H2_plus_mol.set_charge_and_spin(1, 2)
270+
input_file_1 = "H0.qin"
271+
output_file_1 = "H0.qout"
272+
input_file_2 = "H2_plus.qin"
273+
output_file_2 = "H2_plus.qout"
274+
t = []
275+
t.append(
276+
WriteInputFromIOSet(
277+
molecule=H0_atom,
278+
qchem_input_set="SinglePointSet",
279+
input_file=input_file_1,
280+
qchem_input_params=qchem_input_params,
281+
)
282+
)
283+
t.append(
284+
RunQChemCustodian(
285+
qchem_cmd=qchem_cmd,
286+
multimode=multimode,
287+
input_file=input_file_1,
288+
output_file=output_file_1,
289+
max_cores=max_cores,
290+
max_errors=max_errors,
291+
job_type="normal",
292+
gzipped_output=False,
293+
)
294+
)
295+
296+
t.append(
297+
WriteInputFromIOSet(
298+
molecule=H2_plus_mol,
299+
qchem_input_set="SinglePointSet",
300+
input_file=input_file_2,
301+
qchem_input_params=qchem_input_params,
302+
)
303+
)
304+
t.append(
305+
RunQChemCustodian(
306+
qchem_cmd=qchem_cmd,
307+
multimode=multimode,
308+
input_file=input_file_2,
309+
output_file=output_file_2,
310+
max_cores=max_cores,
311+
max_errors=max_errors,
312+
job_type="normal",
313+
gzipped_output=False,
314+
)
315+
)
316+
t.append(
317+
ProtCalcToDb(
318+
db_file=db_file,
319+
input_file_H0=input_file_1,
320+
output_file_H0=output_file_1,
321+
input_file_H2=input_file_2,
322+
output_file_H2=output_file_2,
323+
additional_fields={"task_label": name},
324+
)
325+
)
326+
327+
super().__init__(t, parents=parents, name=name, **kwargs)
328+
329+
213330
class ForceFW(Firework):
214331
def __init__(
215332
self,

atomate/qchem/fireworks/tests/test_core.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
import numpy as np
66
from pymatgen.io.qchem.outputs import QCOutput
7+
from pymatgen.core.structure import Molecule
8+
from pymatgen.core.sites import Site
79

810
from atomate.qchem.firetasks.critic2 import ProcessCritic2, RunCritic2
911
from atomate.qchem.firetasks.fragmenter import FragmentMolecule
1012
from atomate.qchem.firetasks.geo_transformations import PerturbGeometry
1113
from atomate.qchem.firetasks.parse_outputs import QChemToDb
14+
from atomate.qchem.firetasks.parse_outputs import ProtCalcToDb
1215
from atomate.qchem.firetasks.run_calc import RunQChemCustodian
1316
from atomate.qchem.firetasks.write_inputs import WriteInputFromIOSet
1417
from atomate.qchem.fireworks.core import (
@@ -19,6 +22,7 @@
1922
FrequencyFW,
2023
OptimizeFW,
2124
PESScanFW,
25+
ProtonEnergyFW,
2226
SinglePointFW,
2327
TransitionStateFW,
2428
)
@@ -132,6 +136,72 @@ def test_SinglePointFW_not_defaults(self):
132136
self.assertEqual(firework.parents, [])
133137
self.assertEqual(firework.name, "special single point")
134138

139+
def test_ProtonEnergyFW(self):
140+
H_site = Site("H", [0.0, 0.0, 0.0])
141+
H_site_inf = Site("H", [100000.0, 0.0, 0.0])
142+
H0_atom = Molecule.from_sites([H_site])
143+
H2_plus_mol = Molecule.from_sites([H_site, H_site_inf])
144+
H0_atom.set_charge_and_spin(0, 2)
145+
H2_plus_mol.set_charge_and_spin(1, 2)
146+
firework = ProtonEnergyFW(qchem_input_params={"smd_solvent": "water"})
147+
self.assertEqual(
148+
firework.tasks[0].as_dict(),
149+
WriteInputFromIOSet(
150+
molecule=H0_atom,
151+
qchem_input_set="SinglePointSet",
152+
input_file="H0.qin",
153+
qchem_input_params={"smd_solvent": "water"},
154+
).as_dict(),
155+
)
156+
self.assertEqual(
157+
firework.tasks[1].as_dict(),
158+
RunQChemCustodian(
159+
qchem_cmd=">>qchem_cmd<<",
160+
multimode=">>multimode<<",
161+
input_file="H0.qin",
162+
output_file="H0.qout",
163+
max_cores=">>max_cores<<",
164+
max_errors=5,
165+
job_type="normal",
166+
gzipped_output=False,
167+
).as_dict(),
168+
)
169+
self.assertEqual(
170+
firework.tasks[2].as_dict(),
171+
WriteInputFromIOSet(
172+
molecule=H2_plus_mol,
173+
qchem_input_set="SinglePointSet",
174+
input_file="H2_plus.qin",
175+
qchem_input_params={"smd_solvent": "water"},
176+
).as_dict(),
177+
)
178+
self.assertEqual(
179+
firework.tasks[3].as_dict(),
180+
RunQChemCustodian(
181+
qchem_cmd=">>qchem_cmd<<",
182+
multimode=">>multimode<<",
183+
input_file="H2_plus.qin",
184+
output_file="H2_plus.qout",
185+
max_cores=">>max_cores<<",
186+
max_errors=5,
187+
job_type="normal",
188+
gzipped_output=False,
189+
).as_dict(),
190+
)
191+
self.assertEqual(
192+
firework.tasks[4].as_dict(),
193+
ProtCalcToDb(
194+
db_file=None,
195+
input_file_H0="H0.qin",
196+
output_file_H0="H0.qout",
197+
input_file_H2="H2_plus.qin",
198+
output_file_H2="H2_plus.qout",
199+
additional_fields={"task_label": "proton electronic energy"},
200+
).as_dict(),
201+
)
202+
self.assertEqual(firework.parents, [])
203+
self.assertEqual(firework.name, "proton electronic energy")
204+
135205
def test_OptimizeFW_defaults(self):
136206
firework = OptimizeFW(molecule=self.act_mol)
137207
self.assertEqual(

0 commit comments

Comments
 (0)