Skip to content

Commit ea800c6

Browse files
author
Johannes Steinmetzer
committed
mod: implemented get_all_energies for Gaussian16 and added a test
now all OverlapCalculators return the GS energy when root is None
1 parent c376419 commit ea800c6

File tree

7 files changed

+98
-43
lines changed

7 files changed

+98
-43
lines changed

pysisyphus/Geometry.py

+3
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,9 @@ def all_energies(self):
873873
self.set_results(results)
874874
return self._all_energies
875875

876+
def get_root_energy(self, root):
877+
return self.all_energies[root]
878+
876879
def has_all_energies(self):
877880
return self._all_energies is not None
878881

pysisyphus/calculators/Gaussian16.py

+42-15
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(
5555
}
5656
exc_keyword = [key for key in "td tda cis".split() if key in keywords]
5757
self.nstates = self.nroots
58+
self.exc_args = None
5859
if exc_keyword:
5960
self.exc_key = exc_keyword[0]
6061
exc_dict = keywords[self.exc_key]
@@ -101,6 +102,7 @@ def __init__(
101102
self.gaussian_input = textwrap.dedent(self.gaussian_input)
102103

103104
self.parser_funcs = {
105+
"energy": self.parse_energy,
104106
"force": self.parse_force,
105107
"hessian": self.parse_hessian,
106108
"stable": self.parse_stable,
@@ -118,8 +120,7 @@ def __init__(
118120
)
119121

120122
def make_exc_str(self):
121-
# Ground state calculation
122-
if not self.track and (self.root is None):
123+
if self.exc_args is None:
123124
return ""
124125
root = self.root if self.root is not None else 1
125126
root_str = f"root={root}"
@@ -341,7 +342,25 @@ def store_and_track(self, results, func, atoms, coords, **prepare_kwargs):
341342
return results
342343

343344
def get_energy(self, atoms, coords, **prepare_kwargs):
344-
return self.get_forces(atoms, coords, **prepare_kwargs)
345+
did_stable = False
346+
if self.stable:
347+
is_stable = self.run_stable(atoms, coords)
348+
self.log(f"Wavefunction is now stable: {is_stable}")
349+
did_stable = True
350+
inp = self.prepare_input(
351+
atoms, coords, "sp", did_stable=did_stable, **prepare_kwargs
352+
)
353+
kwargs = {
354+
"calc": "energy",
355+
}
356+
results = self.run(inp, **kwargs)
357+
results = self.store_and_track(
358+
results, self.get_energy, atoms, coords, **prepare_kwargs
359+
)
360+
return results
361+
362+
def get_all_energies(self, atoms, coords, **prepare_kwargs):
363+
return self.get_energy(atoms, coords, **prepare_kwargs)
345364

346365
def get_forces(self, atoms, coords, **prepare_kwargs):
347366
did_stable = False
@@ -574,33 +593,41 @@ def prepare_overlap_data(self, path):
574593
all_energies[1:] += exc_energies
575594
return C, X, Y, all_energies
576595

577-
def parse_force(self, path):
596+
def parse_energy(self, path):
578597
results = {}
579-
keys = ("SCF Energy", "Total Energy", "Cartesian Gradient")
598+
keys = ("SCF Energy", "Total Energy")
580599
fchk_path = Path(path) / f"{self.fn_base}.fchk"
581600
fchk_dict = self.parse_fchk(fchk_path, keys)
582601
results["energy"] = fchk_dict["SCF Energy"]
583-
results["forces"] = -fchk_dict["Cartesian Gradient"]
584602

585603
if self.nstates:
586604
# This sets the proper excited state energy in the
587605
# results dict and also stores all energies.
588606
exc_energies = self.parse_tddft(path)
589607
# G16 root input is 1 based, so we substract 1 to get
590608
# the right index here.
591-
root = self.root if self.root is not None else 1
592-
root_exc_en = exc_energies[root - 1]
593609
gs_energy = fchk_dict["SCF Energy"]
594-
# Add excitation energy to ground state energy.
595-
results["energy"] += root_exc_en
596-
# Create a new array including the ground state energy
597-
# to save the energies of all states.
598-
all_ens = np.full(len(exc_energies) + 1, gs_energy)
599-
all_ens[1:] += exc_energies
600-
results["all_energies"] = all_ens
610+
# Modify "energy" when a root is selected
611+
if self.root is not None:
612+
root_exc_en = exc_energies[self.root - 1]
613+
# Add excitation energy to ground state energy.
614+
results["energy"] += root_exc_en
615+
# Create a new array including the ground state energy
616+
# to save the energies of all states.
617+
all_ens = np.full(len(exc_energies) + 1, gs_energy)
618+
all_ens[1:] += exc_energies
619+
results["all_energies"] = all_ens
601620

602621
return results
603622

623+
def parse_force(self, path):
624+
results = self.parse_energy(path)
625+
keys = ("Cartesian Gradient",)
626+
fchk_path = Path(path) / f"{self.fn_base}.fchk"
627+
fchk_dict = self.parse_fchk(fchk_path, keys)
628+
results["forces"] = -fchk_dict["Cartesian Gradient"]
629+
return results
630+
604631
def parse_hessian(self, path):
605632
keys = (
606633
"Total Energy",

pysisyphus/calculators/ORCA.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ def __init__(
616616

617617
self.do_tddft = bool(es_block_header_set & td_blocks)
618618
self.do_ice = bool(es_block_header_set & ice_blocks)
619+
self.do_es = any((self.do_tddft, self.do_ice))
619620
# There can be at most on ES block at a time
620621
assert not (self.do_tddft and self.do_ice)
621622
if self.es_block_header:
@@ -948,7 +949,15 @@ def parse_energy(self, path):
948949
log_fn = log_fn[0]
949950
with open(log_fn) as handle:
950951
text = handle.read()
951-
mobj = re.search(r"FINAL SINGLE POINT ENERGY\s+([\d\-\.]+)", text)
952+
# By default reports the total energy of the first ES, when ES were calculated.
953+
# But we are interested in the GS energy, when self.root is None ...
954+
if not self.do_es or self.root is not None:
955+
en_re = r"FINAL SINGLE POINT ENERGY\s+([\d\-\.]+)"
956+
# ... so we look at the energy that was reported after the SCF.
957+
else:
958+
en_re = "Total Energy\s+:\s+([\d\-\.]+) Eh"
959+
en_re = re.compile(en_re)
960+
mobj = en_re.search(text)
952961
energy = float(mobj[1])
953962
return {"energy": energy}
954963

pysisyphus/calculators/PySCF.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class PySCF(OverlapCalculator):
2323
multisteps = {
2424
"scf": ("scf",),
2525
"tdhf": ("scf", "tddft"),
26+
"tdahf": ("scf", "tda"),
2627
"dft": ("dft",),
2728
"mp2": ("scf", "mp2"),
2829
"tddft": ("dft", "tddft"),
@@ -53,12 +54,13 @@ def __init__(
5354
self.basis = basis
5455
self.xc = xc
5556
self.method = method.lower()
56-
if self.method in ("tda", "tddft") and self.xc is None:
57-
self.multisteps[self.method] = ("scf", self.method)
58-
if self.xc and self.method != "tddft":
57+
self.do_es = self.method in ("tda", "tddft", "tdhf", "tdahf")
58+
# Set method to DFT when an XC-functional was selected, but DFT wasn't explicitly
59+
# enabled. When an ES method was chosen it is kept.
60+
if self.xc and self.method not in ("tddft", "tda"):
5961
self.method = "dft"
60-
if self.method == "tddft":
61-
assert self.nroots, "nroots must be set with method='tddft'!"
62+
if self.do_es:
63+
assert self.nroots, f"nroots must be set with method='{self.method}'!"
6264
self.auxbasis = auxbasis
6365
self.keep_chk = keep_chk
6466
self.verbose = int(verbose)
@@ -164,12 +166,11 @@ def get_energy(self, atoms, coords, **prepare_kwargs):
164166

165167
mol = self.prepare_input(atoms, coords)
166168
mf = self.run(mol, point_charges=point_charges)
167-
energy = mf.e_tot
168-
root = 0 if self.root is None else self.root
169-
try:
170-
energy = energy[root]
171-
except (IndexError, TypeError):
172-
pass
169+
all_energies = self.parse_all_energies()
170+
if self.root is not None:
171+
energy = all_energies[self.root]
172+
else:
173+
energy = all_energies[0]
173174

174175
results = {
175176
"energy": energy,
@@ -191,7 +192,7 @@ def get_forces(self, atoms, coords, **prepare_kwargs):
191192
mol = self.prepare_input(atoms, coords)
192193
mf = self.run(mol, point_charges=point_charges)
193194
grad_driver = mf.Gradients()
194-
if self.root:
195+
if self.root is not None:
195196
grad_driver.state = self.root
196197
gradient = grad_driver.kernel()
197198
self.log("Completed gradient step")

pysisyphus/calculators/Turbomole.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -698,10 +698,11 @@ def parse_energy(self, path):
698698
en_regex = re.compile(r"Total energy\s*:?\s*=?\s*([\d\-\.]+)", re.IGNORECASE)
699699
tot_ens = en_regex.findall(text)
700700

701-
if self.td:
702-
# Drop ground state energy that is repeated
703-
root = self.root if self.root is not None else 1
704-
tot_en = tot_ens[1:][root]
701+
# Only modify energy when self.root is set; otherwise stick with the GS energy.
702+
if self.td and self.root is not None:
703+
# Drop ground state energy that is repeated. That is why we don't subtract
704+
# 1 from self.root.
705+
tot_en = tot_ens[1:][self.root]
705706
elif self.ricc2 and self.ricc2_opt:
706707
results = parse_turbo_gradient(path)
707708
tot_en = results["energy"]

tests/test_es_capabilities/test_es_capabilities.py

+15
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757
},
5858
marks=using("PySCF"),
5959
),
60+
pytest.param(
61+
Gaussian16,
62+
{
63+
"route": "hf def2svp td(nstates=3)",
64+
},
65+
marks=using("gaussian16"),
66+
),
6067
),
6168
)
6269
def test_h2o_all_energies(mult, ref_energies, calc_cls, calc_kwargs, this_dir):
@@ -72,3 +79,11 @@ def test_h2o_all_energies(mult, ref_energies, calc_cls, calc_kwargs, this_dir):
7279
# PySCF and Turbomole agree extermely well, at least in the restricted calculation.
7380
# ORCA deviates up to 5e-5 Eh.
7481
np.testing.assert_allclose(all_energies, ref_energies, atol=5e-5)
82+
83+
# As we did not set any root the geometries energy should correspond to the GS energy
84+
energy = geom.energy
85+
assert energy == pytest.approx(ref_energies[0])
86+
87+
for root in range(4):
88+
root_en = geom.get_root_energy(root)
89+
assert root_en == pytest.approx(ref_energies[root])

tests/test_overlap_calculator/test_calculators.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66

77

88
_ROOT_REF_ENERGIES = {
9+
# -74.9607 au is the GS
910
# -74.4893 au is the S1, not the GS
10-
(ORCA, None): -74.4893,
11+
(ORCA, None): -74.9607,
1112
(ORCA, 2): -74.3984,
12-
(PySCF, None): -74.4893,
13-
(PySCF, 2): -74.3714,
14-
(DFTBp, None): -4.077751,
15-
(DFTBp, 2): -3.33313,
16-
(Turbomole, None): -74.48927,
13+
(PySCF, None): -74.9607,
14+
(PySCF, 2): -74.3984, # ???
15+
(Turbomole, None): -74.9607,
1716
(Turbomole, 2): -74.39837,
18-
(Gaussian16, None): -74.48927,
17+
(Gaussian16, None): -74.9607,
1918
(Gaussian16, 2): -74.39837,
19+
(DFTBp, None): -4.077751,
20+
(DFTBp, 2): -3.33313,
2021
}
2122

2223

@@ -35,7 +36,7 @@
3536
PySCF,
3637
{
3738
"basis": "sto3g",
38-
"method": "tddft",
39+
"method": "tdhf",
3940
"nroots": 3,
4041
},
4142
marks=(using("pyscf")),
@@ -63,9 +64,7 @@
6364
),
6465
pytest.param(
6566
Gaussian16,
66-
{
67-
"route": "hf sto-3g td=(nstates=3)"
68-
},
67+
{"route": "hf sto-3g td=(nstates=3)"},
6968
marks=(using("gaussian16")),
7069
),
7170
),

0 commit comments

Comments
 (0)