|
| 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