Skip to content

Commit 69ad658

Browse files
author
Rishabh Guha
committed
added the proton electronic energy firework
1 parent 68f63db commit 69ad658

File tree

5 files changed

+303
-8
lines changed

5 files changed

+303
-8
lines changed

atomate/qchem/firetasks/parse_outputs.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,105 @@ 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+
multirun (bool): Whether the job to parse includes multiple
140+
calculations in one input / output pair.
141+
runs (list): Series of file suffixes that the Drone should look for
142+
when parsing output.
143+
"""
144+
145+
optional_params = [
146+
"calc_dir",
147+
"calc_loc",
148+
"input_file_H0",
149+
"output_file_H0",
150+
"input_file_H2",
151+
"output_file_H2",
152+
"additional_fields",
153+
"db_file",
154+
"multirun",
155+
"runs",
156+
]
157+
158+
def run_task(self, fw_spec):
159+
# get the directory that contains the QChem dir to parse
160+
calc_dir = os.getcwd()
161+
if "calc_dir" in self:
162+
calc_dir = self["calc_dir"]
163+
elif self.get("calc_loc"):
164+
calc_dir = get_calc_loc(self["calc_loc"], fw_spec["calc_locs"])["path"]
165+
input_file_H0 = self.get("input_file_H0", "H0.qin")
166+
output_file_H0 = self.get("output_file_H0", "H0.qout")
167+
input_file_H2 = self.get("input_file_H2", "H2_plus.qin")
168+
output_file_H2 = self.get("output_file_H2", "H2_plus.qout")
169+
multirun = self.get("multirun", False)
170+
runs = self.get("runs", None)
171+
172+
# parse the QChem directory
173+
logger.info(f"PARSING DIRECTORY: {calc_dir}")
174+
175+
additional_fields = self.get("additional_fields", {})
176+
177+
drone = QChemDrone(runs=runs, additional_fields=additional_fields)
178+
179+
# assimilate (i.e., parse)
180+
task_doc_1 = drone.assimilate(
181+
path=calc_dir,
182+
input_file=input_file_H0,
183+
output_file=output_file_H0,
184+
multirun=multirun,
185+
)
186+
187+
task_doc_2 = drone.assimilate(
188+
path=calc_dir,
189+
input_file=input_file_H2,
190+
output_file=output_file_H2,
191+
multirun=multirun,
192+
)
193+
194+
task_doc_clean = task_doc_1
195+
task_doc_clean["calcs_reversed"].append(task_doc_2["calcs_reversed"][0])
196+
task_doc_clean["input"]["initial_molecule"]["charge"] = 1
197+
task_doc_clean["input"]["initial_molecule"]["spin_multiplicity"] = 1
198+
task_doc_clean["orig"]["molecule"]["charge"] = 1
199+
task_doc_clean["orig"]["molecule"]["spin_multiplicity"] = 1
200+
task_doc_clean["output"]["initial_molecule"]["charge"] = 1
201+
task_doc_clean["output"]["initial_molecule"]["spin_multiplicity"] = 1
202+
task_doc_clean["output"]["final_energy"] = (
203+
task_doc_2["output"]["final_energy"] - task_doc_1["output"]["final_energy"]
204+
)
205+
206+
# get the database connection
207+
db_file = env_chk(self.get("db_file"), fw_spec)
208+
209+
# db insertion or taskdoc dump
210+
if not db_file:
211+
with open(os.path.join(calc_dir, "task.json"), "w") as f:
212+
f.write(json.dumps(task_doc_clean, default=DATETIME_HANDLER))
213+
else:
214+
mmdb = QChemCalcDb.from_db_file(db_file, admin=True)
215+
t_id = mmdb.insert(task_doc_clean)
216+
logger.info(f"Finished parsing with task_id: {t_id}")
217+
218+
return FWAction(
219+
stored_data={"task_id": task_doc_clean.get("task_id", None)},
220+
)

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

@@ -99,6 +101,121 @@ def __init__(
99101
super().__init__(t, parents=parents, name=name, **kwargs)
100102

101103

104+
class ProtonEnergyFW(Firework):
105+
def __init__(
106+
self,
107+
name="proton electronic energy",
108+
qchem_cmd=">>qchem_cmd<<",
109+
multimode=">>multimode<<",
110+
max_cores=">>max_cores<<",
111+
qchem_input_params=None,
112+
db_file=None,
113+
parents=None,
114+
max_errors=5,
115+
**kwargs
116+
):
117+
"""
118+
For this custom Firework the electronic energy of a proton in a specific solvent environment is approximated.
119+
Since a proton has 0 electrons,running a QChem job would yield an error. The energy can be approximated by
120+
calculating the electronic energy of a proton and a hydrogen atom at infinite separation and then subtracting
121+
the electronic energy of a hydrogen atom. This Firework combines these two calculations and adds a task doc to the
122+
DB with the separate calculation details and the effective energy after subtraction.
123+
124+
Args:
125+
name (str): Name for the Firework.
126+
qchem_cmd (str): Command to run QChem. Supports env_chk.
127+
multimode (str): Parallelization scheme, either openmp or mpi. Supports env_chk.
128+
max_cores (int): Maximum number of cores to parallelize over. Supports env_chk.
129+
qchem_input_params (dict): Specify kwargs for instantiating the input set parameters
130+
calculating the electronic energy of the proton in a specific solvent environment.
131+
The energy of the proton will be effectively zero in vacuum. Use either pcm_dieletric
132+
or some smd_solvent.Basic uses would be to modify the default inputs of the set,
133+
such as dft_rung, basis_set, pcm_dielectric, scf_algorithm, or max_scf_cycles.
134+
See pymatgen/io/qchem/sets.py for default values of all input parameters. For
135+
instance, if a user wanted to use a more advanced DFT functional, include a pcm
136+
with a dielectric of 30, and use a larger basis, the user would set
137+
qchem_input_params = {"dft_rung": 5, "pcm_dielectric": 30, "basis_set":
138+
"6-311++g**"}. However, more advanced customization of the input is also
139+
possible through the overwrite_inputs key which allows the user to directly
140+
modify the rem, pcm, smd, and solvent dictionaries that QChemDictSet passes to
141+
inputs.py to print an actual input file. For instance, if a user wanted to set
142+
the sym_ignore flag in the rem section of the input file to true, then they
143+
would set qchem_input_params = {"overwrite_inputs": "rem": {"sym_ignore":
144+
"true"}}. Of course, overwrite_inputs could be used in conjunction with more
145+
typical modifications, as seen in the test_double_FF_opt workflow test.
146+
db_file (str): Path to file specifying db credentials to place output parsing.
147+
parents ([Firework]): Parents of this particular Firework.
148+
**kwargs: Other kwargs that are passed to Firework.__init__.
149+
"""
150+
151+
qchem_input_params = qchem_input_params or {}
152+
153+
H_site = Site("H", [0.0, 0.0, 0.0])
154+
H_site_inf = Site("H", [100000.0, 0.0, 0.0])
155+
H0_atom = Molecule.from_sites([H_site])
156+
H2_plus_mol = Molecule.from_sites([H_site, H_site_inf])
157+
H0_atom.set_charge_and_spin(0, 2)
158+
H2_plus_mol.set_charge_and_spin(1, 2)
159+
input_file_1 = "H0.qin"
160+
output_file_1 = "H0.qout"
161+
input_file_2 = "H2_plus.qin"
162+
output_file_2 = "H2_plus.qout"
163+
t = []
164+
t.append(
165+
WriteInputFromIOSet(
166+
molecule=H0_atom,
167+
qchem_input_set="SinglePointSet",
168+
input_file=input_file_1,
169+
qchem_input_params=qchem_input_params,
170+
)
171+
)
172+
t.append(
173+
RunQChemCustodian(
174+
qchem_cmd=qchem_cmd,
175+
multimode=multimode,
176+
input_file=input_file_1,
177+
output_file=output_file_1,
178+
max_cores=max_cores,
179+
max_errors=max_errors,
180+
job_type="normal",
181+
gzipped_output=False,
182+
)
183+
)
184+
185+
t.append(
186+
WriteInputFromIOSet(
187+
molecule=H2_plus_mol,
188+
qchem_input_set="SinglePointSet",
189+
input_file=input_file_2,
190+
qchem_input_params=qchem_input_params,
191+
)
192+
)
193+
t.append(
194+
RunQChemCustodian(
195+
qchem_cmd=qchem_cmd,
196+
multimode=multimode,
197+
input_file=input_file_2,
198+
output_file=output_file_2,
199+
max_cores=max_cores,
200+
max_errors=max_errors,
201+
job_type="normal",
202+
gzipped_output=False,
203+
)
204+
)
205+
t.append(
206+
ProtCalcToDb(
207+
db_file=db_file,
208+
input_file_H0=input_file_1,
209+
output_file_H0=output_file_1,
210+
input_file_H2=input_file_2,
211+
output_file_H2=output_file_2,
212+
additional_fields={"task_label": name},
213+
)
214+
)
215+
216+
super().__init__(t, parents=parents, name=name, **kwargs)
217+
218+
102219
class ForceFW(Firework):
103220
def __init__(
104221
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_not_defaults(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)