# -*- coding: utf-8 -*-
"""An orca input generator and reader
This module reads in a configuration file and generates the requisite input
files. It also reads existing configuration files and returns interesting
things. A short description of the input types is at the `ORCA input
description`_ page.
Example:
See the tests for more
$ pytest
Some more details.
Todo:
* Make tests
* Return interesting things
* Add MNDO and other semi-empirical methods, which employ a minimal basis by
default and do not need a basis set in the input
* Add more explicit support for "simple input lines"
* Add more explicit support for the "block input structure"
* Add support for "sequential" jobs
* Support multiple xyz files [DONE]
* Clean up geometry, add gen_dirs back
* Test and expand brokensym
* Test number in harness
* Test Visualizer modifications
* Validate scans and constraints
* Parse wailord generated input files
.. _Google Python Style Guide:
http://google.github.io/styleguide/pyguide.html
.. _ORCA input description:
https://sites.google.com/site/orcainputlibrary/generalinput
"""
import wailord.io as waio
import wailord.utils as wau
import os
import shutil
import textwrap
import warnings
import itertools as itertt
from pathlib import Path
from operator import itemgetter
from konfik import Konfik
SCAN_TYPES = {"D": "Dihedral", "B": "Bond", "A": "Angle"}
CONSTRAINT_TYPES = {"D": "Dihedral", "B": "Bond", "A": "Angle", "C": "Cartesian"}
AXIS_PROXY = {"x": 1, "y": 2, "z": 3} # 0 is the atom type
[docs]class inpGenerator:
def __init__(self, filename):
self.qc = None
self.xyz = []
self.xyzpath = None
self.spin = None
self.extra = None
self.conf_path = Path(filename)
self.geomlines = None
self.xyzlines = []
self.prjname = "wailordFold"
self.scf = None
self.viz = None
self.paramlines = None
self.keylines = None
self.blocks = None
self.konfik = Konfik(config_path=filename)
self.scripts = []
def __repr__(self):
return f"{self.konfik.show_config()}"
[docs] def read_yml(self):
""" Returns the overall output. """
self.qc, self.xyzpath, self.spin = itemgetter("qc", "xyz", "spin")(
self.konfik.config
)
# Test later
if "basis_sets" not in self.qc:
self.qc["basis_sets"] = ["semi_emp"]
# Test this later, with and without, also try overrides
if "extra" in self.konfik.config.keys():
self.extra = self.konfik.config.extra
else:
print("Consider using None in the yml file for extra")
[docs] def parse_geom(self, geom):
"""Rework the geometry into output"""
textlines = []
textlines.append("\n%geom")
if "scan" in geom:
textlines.append(f"\n Scan\n")
for scanthing in geom.scan.keys():
if scanthing.capitalize() in SCAN_TYPES.values():
textlines.append(
self.geom_scan(geom.scan[f"{scanthing}"], scantype=scanthing)
)
else:
raise TypeError(
f"Only dihedral, bond and angle scan types are supported, got {scanthing} instead"
)
textlines.append(f" end")
if "constrain" in geom:
textlines.append(f"\n Constraints\n")
for consthing in geom.constrain.keys():
if consthing.capitalize() in CONSTRAINT_TYPES.values():
textlines.append(
self.geom_constrain(
geom.constrain[f"{consthing}"], constype=consthing
)
)
else:
raise TypeError(
f"Only dihedral, bond, angle, and cartesian constraints are supported, got {consthing} instead"
)
textlines.append(f" end")
if "maxiter" in geom:
string = f"""
maxiter {geom.maxiter}
"""
textlines.append(textwrap.dedent(string))
textlines.append("\nend\n")
return "".join(textlines)
[docs] def geom_constrain(self, cons, constype):
"""Handles constraints"""
linesthing = []
constype = constype[0][0].upper()
for constraint in cons:
btwn = constraint["between"]
if "value" in constraint.keys():
value = float(constraint["value"])
else:
value = ""
if constype != "C":
btwn_num = btwn.split(" ")
# TODO Handle the other types as tuples (B,2)
comment = self.scan_comment(
btwn,
thistype=constype,
use_types=CONSTRAINT_TYPES,
usage="constraint on",
)
# if constype == 'B' and len(btwn_num) == 2:
else:
warnings.warn(
f"Cartesian comments have not been implemented", UserWarning
)
comment = ""
linesthing.append(f" {{ {constype} {btwn} {value} C }} # {comment}\n")
return "".join(linesthing)
[docs] def geom_scan(self, thing, scantype):
"""Handles scans"""
linesthing = []
scantype = scantype[0][0].upper()
for thingline in thing:
btwn, fromto, points = itemgetter(
"between",
"range",
"points",
)(thingline)
comment = self.scan_comment(btwn, scantype)
linesthing.append(
f" {scantype} {btwn} = {fromto[0]}, {fromto[1]}, {points} # {comment}\n"
)
return "".join(linesthing)
[docs] def parse_xyz(self):
"""Generate folder structure for each system"""
self.xyzpath = Path(self.xyzpath)
if not self.xyzpath.is_dir():
self.xyz.append(
waio.xyz.xyzIO(self.conf_path.parent / self.konfik.config.xyz)
)
self.xyzlines.append(self.xyz[-1].xyzdat.coord_block)
# Geometry
if "geom" in self.konfik.config.keys():
self.geomlines = self.parse_geom(self.konfik.config.geom)
# Paramter Blocks
if "params" in self.konfik.config.keys():
self.paramlines = self.parse_params(self.konfik.config.params)
if "scf" in self.konfik.config.keys():
self.scf = self.parse_scf(self.konfik.config.scf)
if "viz" in self.konfik.config.keys():
self.viz = self.parse_viz(self.konfik.config.viz)
if "keywords" in self.konfik.config.keys():
self.keylines = self.parse_keywords(self.konfik.config.keywords)
if "blocks" in self.konfik.config.keys():
self.blocks = self.parse_blocks(self.konfik.config.blocks)
self.gendir_qc(extra=None)
else:
for root, dirs, files in os.walk(self.xyzpath.resolve()):
for filename in files:
if Path(filename).suffix == ".xyz":
self.xyz.append(
waio.xyz.xyzIO(self.conf_path.parent / f"{root}/{filename}")
)
self.xyzlines.append(self.xyz[-1].xyzdat.coord_block)
# Geometry
if "geom" in self.konfik.config.keys():
self.geomlines = self.parse_geom(self.konfik.config.geom)
# Paramter Blocks
if "params" in self.konfik.config.keys():
self.paramlines = self.parse_params(
self.konfik.config.params
)
if "scf" in self.konfik.config.keys():
self.scf = self.parse_scf(self.konfik.config.scf)
if "viz" in self.konfik.config.keys():
self.viz = self.parse_viz(self.konfik.config.viz)
if "keywords" in self.konfik.config.keys():
self.keylines = self.parse_keywords(
self.konfik.config.keywords
)
if "blocks" in self.konfik.config.keys():
self.blocks = self.parse_blocks(self.konfik.config.blocks)
self.gendir_qc(extra=None)
pass
[docs] def parse_blocks(self, blocks):
"""Generic Block Handler
Consider the following:
blocks:
- method:
- Z_Tol: "1e-14"
- SpecialGridAtoms: "28, 29, 27"
- SpecialGridIntacc: "8, 8, 8"
Which is to be mapped to:
%method
Z_Tol 1e-14
SpecialGridAtoms 28, 29, 27
SpecialGridIntacc 8, 8, 8
end
Args:
None
Returns:
blocks (`list` of `str`): The blocks to be rendered
"""
blines = []
for bname in blocks:
for bid, bdat in bname.items():
blines.append(f"\n%{bid}\n")
for line in bdat:
for key, value in line.items():
string = f" {key} {value}\n"
blines.append(string)
blines.append("end\n")
return "".join(blines)
[docs] def parse_keywords(self, keywords):
keylines = []
for kl in keywords:
keylines.append(f"\n! {kl}")
return "".join(keylines)
[docs] def parse_yml(self):
"""Handle the various options"""
if self.spin is None:
# Deal with parsing the basic structure
self.read_yml()
self.parse_xyz()
if self.qc.active is True:
self.parse_qc()
pass
[docs] def parse_viz(self, viz):
if viz.chemcraft is True:
string = """
%output
Print[ P_Basis ] 2
Print[ P_MOs ] 1
end
"""
return textwrap.dedent(string)
[docs] def parse_scf(self, scf):
textlines = []
textlines.append("\n%scf\n")
if "brokensym" in scf:
textlines.append(
f" BrokenSym {scf.brokensym.more_unpaired}, {scf.brokensym.less_unpaired}"
)
if "maxiter" in scf:
string = f"""
maxiter {scf.maxiter}
"""
textlines.append(textwrap.dedent(string))
textlines.append("\nend\n")
return "".join(textlines)
[docs] def parse_params(self, params):
"""Rework the parameters into output. Recall that these do not require
relaxation, and can also take fixed variables. On the other hand, these
need more information to set up."""
textlines = []
textlines.append("%paras\n")
for param in params:
if param.get("value"):
param["slot"]["name"] = param["name"]
self.xyzlines[-1], comment = self.params_slot(param["slot"])
textlines.append(self.params_value(param, comment))
elif param.get("range"):
param["slot"]["name"] = param["name"]
self.xyzlines[-1], comment = self.params_slot(param["slot"])
textlines.append(self.params_range(param, comment))
else:
raise TypeError(
f"Currently only value and range variables are supported"
)
textlines.append("end\n")
return "".join(textlines)
[docs] def params_slot(self, thing):
if not "xyz" in thing or thing["xyz"] is not True:
raise TypeError("Currently only supports xyz")
atype, anum, axis, name = itemgetter("atype", "anum", "axis", "name")(thing)
xyztmp = self.xyzlines[-1].split("\n")
aline = xyztmp[anum].split()
if aline[0] == atype:
aline[AXIS_PROXY[axis]] = f"{{{name}}}".ljust(15, " ")
xyztmp[anum] = " ".join(aline)
# print("\n".join(xyztmp))
xyzblock = "\n".join(xyztmp)
comment = f"# {axis}-axis of {atype}{anum}"
return xyzblock, comment
[docs] def params_value(self, thing, comment):
name, val = itemgetter("name", "value")(thing)
return f"\t{name} = {val} {comment}\n"
[docs] def params_range(self, thing, comment):
"""Handles variables with range"""
name, lrange, points = itemgetter("name", "range", "points")(thing)
return f"\t{name} = {lrange[0]}, {lrange[1]}, {points} {comment}\n"
[docs] def genharness(self, basename, slow=False):
"""
Generate a harness file.
Args:
basename (str): The folder into which the harness should be put.
slow (bool): A parameter used to ensure better practices, rate limits to submitting 10 files every 30 seconds
Returns:
Nothing: This generates a file, and nothing else
"""
with open(f"{basename.parent.parent}/harness.sh", "w") as op:
op.write("#!/usr/bin/env bash\n")
op.write("export cur_file=$(realpath $0) \n")
op.write('export cur_dir=$(dirname "${cur_file}")\n')
for num, script in enumerate(self.scripts, start=1):
op.write(f'cd "./{script.parents[0]}"')
op.write("\n")
op.write(f"qsub './{script.name}'")
op.write("\n")
op.write("cd $cur_dir\n")
op.write("\n")
if slow is True:
if num % 10 == 0:
op.write('echo("Slowing down!")\n')
op.write("sleep 30s\n")
pass
[docs] def parse_qc(self):
qcList = list(
itertt.chain(self.qc.style, self.qc.calculations, self.qc.basis_sets)
)
self.qcopts = qcList
pass
[docs] def gendir_qc(self, basename=None, extra=None):
"""Function to generate QC folder structure recursively"""
if basename is None:
basename = Path(f"{self.prjname}/{self.xyz[-1].slug}")
if extra is not None:
print("Overwriting extra from yml file")
self.extra = extra
for styl in self.konfik.config.qc.style:
styl = styl.replace(" ", "_")
self.gendir_qcspin(basename / styl)
self.genharness(basename)
pass
[docs] def gendir_qcspin(self, path):
"""Generates the style folders"""
for sp in self.spin:
sp = sp.replace(" ", "")
self.gendir_qccalc(path / Path(f"spin_{sp}"))
pass
[docs] def gendir_qccalc(self, path):
"""Generates set of calculation folders"""
for cal in self.konfik.config.qc.calculations:
self.gendir_qcbasis(path / cal)
pass
[docs] def gendir_qcbasis(self, path):
"""Generates the directory of input files. Note that the folders will have +
replaced by P and * by 8"""
for base in self.konfik.config.qc.basis_sets:
bas = base.replace("+", "P").replace("*", "8")
Path.mkdir(path / bas, parents=True, exist_ok=True)
self.geninp(path / bas)
pass
[docs] def geninp(self, path):
"""Uses the path to generate details for an input file"""
tmpstr = str(path).split("/")
tmpconf = {
# Reverse the function call order
"basis": tmpstr[-1].replace("P", "+").replace("8", "*"),
"calc": tmpstr[-2],
"spin": " ".join(
list(itertt.chain.from_iterable(tmpstr[-3].replace("spin_", "")))
),
"style": tmpstr[-4].replace("_", " "),
"name": path / "orca.inp",
"unrestricted": False,
}
if self.scf is not None:
if tmpconf["style"].find("UKS") != -1 or tmpconf["style"].find("UHF") != -1:
tmpconf["unrestricted"] = True
self.writeinp(tmpconf)
self.putscript(
to_loc=path,
from_loc=self.conf_path.parent / self.konfik.config.jobscript,
slug=f"{tmpconf['basis']}_{tmpconf['style']}",
)
pass
[docs] def putscript(self, from_loc, to_loc, slug):
"""Copies the jobscript"""
shutil.copy(from_loc, to_loc)
scriptname = to_loc / self.konfik.config.jobscript
slug = slug.replace(" ", "_")
rep_obj = {
"prev": ["ORCA_CALCULATION"],
"to": [slug],
}
wau.repkey(scriptname, rep_obj)
if self.viz is not None:
if self.konfik.config.viz.chemcraft is True:
with open(scriptname, "a") as script:
string = """
cd $SLURM_SUBMIT_DIR
$orcadir/orca_2mkl orca -molden"""
script.writelines(textwrap.dedent(string))
self.scripts.append(scriptname)
pass
[docs] def writeinp(self, confobj, extralines=None):
"""Writes an input file. Minimally should have:
basis, calc, spin, style, name
extralines: Optional set of lines to write out before the coordinate block
"""
if extralines is None:
extralines = self.extra
basis, calc, spin, style, name, unrestricted = itemgetter(
"basis", "calc", "spin", "style", "name", "unrestricted"
)(confobj)
if basis == "semi_emp":
basis = ""
with open(name, "w") as op:
op.write(f"!{style} {basis} {calc}\n")
if self.keylines is not None:
op.writelines(self.keylines)
op.write("\n")
if extralines is not None:
op.writelines(extralines)
op.write("\n")
if unrestricted is True:
op.writelines(self.scf)
op.write("\n")
if self.viz is not None:
op.writelines(self.viz)
if self.paramlines is not None:
op.writelines(self.paramlines)
if self.blocks is not None:
op.writelines(self.blocks)
if self.geomlines is not None:
op.writelines(self.geomlines)
op.write("\n")
op.write(f"\n*xyz {spin}")
op.write("\n")
op.write(self.xyzlines[-1])
op.write("*")
pass
[docs]class coordBlock:
"""Base class for the coordinate block"""
def __init__(self, data):
self.keyword = None
self.lines = None
return
def __repr__(self):
string = f"""
*block {keyword}
{lines}
*
"""
return textwrap.dedent(string)