Skip to content

Commit 050c3d5

Browse files
andyclaude
andcommitted
Step 6: adjudicate disputed basis sets against the codes' own copies
Comparing ECCE against BSE could only ever say THAT they differ, never which is right -- there was no reference. Andy's suggestion: the codes ship their own copies, so run the same single atom four ways, by name in ORCA and in Gaussian and explicitly from each of ECCE and BSE. Where the two built-ins agree they are an independent reference, and whichever source reproduces them is correct. The answer is consistent and not comfortable. In every case tested, the built-ins agree with each other to within 2e-09 Hartree, BSE reproduces them, and ECCE'S SHIPPED DATA DOES NOT: basis element ECCE off by BSE off by Ahlrichs VDZ Kr 0.917 Ha 0.0 Ahlrichs VDZ Br 0.878 Ha 0.0 Ahlrichs VDZ Cr 0.186 Ha 0.0 Ahlrichs TZV Mn 0.459 Ha 3.8e-06 cc-pVQZ-DK Sc 1.78 Ha 9.9e-05 cc-pV6Z S 4.2e-06 Ha 9.5e-10 Five of those six are not rounding. 0.917 Hartree is 575 kcal/mol. So the shipped library contains real errors for these elements, and re-sourcing corrects them rather than merely modernising them. Also corrects one of my own findings. J-DZP-DKH was reported as a constant ratio of exactly -1.000000, read as a sign convention. It is not: ECCE's Na S exponents start at 27.6208 where BSE's start at 10.1167, and Mg's two lists are offset by one entry. The shells are misaligned, so the comparison was matching different rows and the "ratio" was an artifact of that. The constant-ratio heuristic can fire on misaligned data and produce a meaningless number; it is evidence to look, never a conclusion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFMUNbw2UpRPmgsqviAowb
1 parent f160bfd commit 050c3d5

1 file changed

Lines changed: 196 additions & 0 deletions

File tree

‎tools/basissets/adjudicate.py‎

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Adjudicate a disputed basis set against the codes' own built-in copies.
4+
5+
Step 6 of the re-sourcing plan (issue #116). Comparing ECCE against BSE
6+
only ever says THAT they differ, never which is right -- there was no
7+
reference. The codes ship their own copies of these basis sets, so:
8+
9+
run the same single atom four ways
10+
1. ORCA's built-in basis, by name
11+
2. Gaussian's built-in basis, by name
12+
3. ECCE's stored data, written out explicitly
13+
4. BSE's data, written out explicitly
14+
15+
If the two built-ins agree with each other they are an independent
16+
reference, and whichever of ECCE and BSE reproduces them is correct.
17+
If the built-ins disagree, there is no reference and the difference is a
18+
genuine ambiguity in the literature rather than an error in either.
19+
20+
A single atom is used on purpose: it isolates the element that actually
21+
differs, and removes geometry as a variable.
22+
"""
23+
24+
import argparse
25+
import json
26+
import os
27+
import re
28+
import subprocess
29+
import sys
30+
import tempfile
31+
import urllib.parse
32+
import urllib.request
33+
34+
HERE = os.path.dirname(os.path.abspath(__file__))
35+
sys.path.insert(0, HERE)
36+
from bse2bas import convert # noqa: E402
37+
from diffreport import parse_bas, parse_text, SYM # noqa: E402
38+
39+
ORCA = "/opt/orca/orca_6_1_1_linux_x86-64_shared_openmpi418_nodmrg/orca"
40+
LIB = os.path.expanduser("~/.ECCE/dataserver/htdocs/Ecce/system/"
41+
"GaussianBasisSetLibrary")
42+
LETTER = "SPDFGHI"
43+
44+
# Ground-state multiplicity, so a single atom is a valid calculation.
45+
MULT = {"H": 2, "He": 1, "C": 3, "N": 4, "O": 3, "S": 3, "Ne": 1, "Ar": 1,
46+
"Kr": 1, "Xe": 1, "Sc": 2, "Ti": 3, "V": 4, "Cr": 7, "Mn": 6,
47+
"Fe": 5, "Co": 4, "Ni": 3, "Cu": 2, "Zn": 1, "Br": 2, "Mo": 7,
48+
"I": 2, "Ag": 2, "Ca": 1, "Mg": 1, "Al": 2, "Si": 3, "P": 4}
49+
50+
51+
def orca_newgto(element, shells):
52+
"""One atom's shells as an ORCA NewGTO block.
53+
54+
ORCA takes one coefficient per line, so a general contraction has to
55+
be split into a separate block per coefficient column, and a column's
56+
zero entries dropped -- which is what wrORCAGBS.pm does.
57+
"""
58+
out = ["NewGTO %s" % element]
59+
for shell, nprim, ncoef, rows in shells:
60+
letter = shell[0]
61+
if letter not in LETTER:
62+
return None # SP and friends: not handled
63+
for column in range(ncoef):
64+
kept = [(r[0], r[column + 1]) for r in rows
65+
if len(r) > column + 1 and float(r[column + 1]) != 0.0]
66+
if not kept:
67+
continue
68+
out.append(" %s %d" % (letter, len(kept)))
69+
for i, (e, c) in enumerate(kept, 1):
70+
out.append(" %d %.10f %.10f" % (i, e, c))
71+
out.append("end")
72+
return "\n".join(out)
73+
74+
75+
def run_orca(element, multiplicity, basis_line, basis_block):
76+
body = "! RHF UHF\n" if multiplicity > 1 else "! RHF\n"
77+
body = "! %s\n" % ("UHF" if multiplicity > 1 else "RHF")
78+
if basis_line:
79+
body = "! %s %s\n" % ("UHF" if multiplicity > 1 else "RHF", basis_line)
80+
if basis_block:
81+
body += "%%basis\n%s\nend\n" % basis_block
82+
body += "* xyz 0 %d\n%s 0.0 0.0 0.0\n*\n" % (multiplicity, element)
83+
with tempfile.TemporaryDirectory() as directory:
84+
path = os.path.join(directory, "a.inp")
85+
open(path, "w").write(body)
86+
try:
87+
proc = subprocess.run([ORCA, path], capture_output=True,
88+
text=True, timeout=600, cwd=directory)
89+
except subprocess.TimeoutExpired:
90+
return None, "timeout"
91+
for line in proc.stdout.splitlines():
92+
if "FINAL SINGLE POINT ENERGY" in line:
93+
return float(line.split()[4]), None
94+
err = next((l.strip() for l in proc.stdout.splitlines()
95+
if "Error" in l or "aborting" in l), "no energy")
96+
return None, err[:70]
97+
98+
99+
def run_g16(element, multiplicity, basis_name):
100+
body = ("#P %s/%s SP\n\nt\n\n0 %d\n%s 0.0 0.0 0.0\n\n"
101+
% ("UHF" if multiplicity > 1 else "RHF", basis_name,
102+
multiplicity, element))
103+
env = dict(os.environ, g16root="/opt/gaussian")
104+
with tempfile.TemporaryDirectory() as directory:
105+
path = os.path.join(directory, "a.gjf")
106+
open(path, "w").write(body)
107+
try:
108+
proc = subprocess.run(
109+
"source /opt/gaussian/g16/bsd/g16.profile >/dev/null 2>&1; "
110+
"g16 < %s" % path, shell=True, executable="/bin/bash",
111+
capture_output=True, text=True, timeout=600, cwd=directory,
112+
env=env)
113+
except subprocess.TimeoutExpired:
114+
return None, "timeout"
115+
for line in proc.stdout.splitlines():
116+
m = re.match(r"\s*SCF Done:\s+E\(\S+\)\s*=\s*(\S+)", line)
117+
if m:
118+
return float(m.group(1)), None
119+
return None, "no energy"
120+
121+
122+
def main():
123+
ap = argparse.ArgumentParser()
124+
ap.add_argument("basis", help="ECCE's name for the basis set")
125+
ap.add_argument("element")
126+
ap.add_argument("--orca-name", help="the basis name ORCA knows it by")
127+
ap.add_argument("--g16-name", help="the basis name Gaussian knows it by")
128+
ap.add_argument("--multiplicity", type=int)
129+
args = ap.parse_args()
130+
131+
element = args.element
132+
multiplicity = args.multiplicity or MULT.get(element)
133+
if not multiplicity:
134+
print("no ground-state multiplicity known for %s; pass "
135+
"--multiplicity" % element, file=sys.stderr)
136+
return 2
137+
138+
baseline = json.load(open(os.path.join(HERE, "baseline.json")))
139+
namemap = json.load(open(os.path.join(HERE, "namemap.json")))
140+
filename = bse_name = None
141+
for entries in baseline.values():
142+
if args.basis in entries:
143+
filename = entries[args.basis]["files"][0]
144+
for bucket in ("direct", "alias", "curated"):
145+
if args.basis in namemap[bucket]:
146+
bse_name = namemap[bucket][args.basis]
147+
if not filename or not bse_name:
148+
print("unknown basis: %s" % args.basis, file=sys.stderr)
149+
return 2
150+
151+
shipped = parse_bas(os.path.join(LIB, filename))
152+
url = ("https://www.basissetexchange.org/api/basis/%s/format/json/"
153+
% urllib.parse.quote(bse_name))
154+
with urllib.request.urlopen(url, timeout=90) as response:
155+
regen = parse_text(convert(json.load(response), SYM))
156+
157+
print("%s / %s (multiplicity %d)" % (args.basis, element, multiplicity))
158+
print(" ECCE file : %s" % filename)
159+
print(" BSE name : %s\n" % bse_name)
160+
161+
results = {}
162+
if args.orca_name:
163+
e, err = run_orca(element, multiplicity, args.orca_name, None)
164+
results["ORCA built-in"] = (e, err)
165+
if args.g16_name:
166+
e, err = run_g16(element, multiplicity, args.g16_name)
167+
results["G16 built-in"] = (e, err)
168+
for label, source in (("ECCE data", shipped), ("BSE data", regen)):
169+
block = orca_newgto(element, source.get(element, []))
170+
if block is None:
171+
results[label] = (None, "shell type not expressible in NewGTO")
172+
else:
173+
results[label] = run_orca(element, multiplicity, None, block)
174+
175+
for label in ("ORCA built-in", "G16 built-in", "ECCE data", "BSE data"):
176+
if label not in results:
177+
continue
178+
energy, err = results[label]
179+
print(" %-14s %s" % (label,
180+
"%.9f" % energy if energy is not None
181+
else "FAILED (%s)" % err))
182+
183+
built = [results[k][0] for k in ("ORCA built-in", "G16 built-in")
184+
if k in results and results[k][0] is not None]
185+
if len(built) == 2:
186+
print("\n built-ins differ by %.2e Hartree" % abs(built[0] - built[1]))
187+
for label in ("ECCE data", "BSE data"):
188+
if results.get(label, (None,))[0] is None or not built:
189+
continue
190+
print(" %-14s vs ORCA built-in: %.2e Hartree"
191+
% (label, abs(results[label][0] - built[0])))
192+
return 0
193+
194+
195+
if __name__ == "__main__":
196+
sys.exit(main())

0 commit comments

Comments
 (0)