Skip to content

Commit

Permalink
Merge pull request #8 from berquist/qchem
Browse files Browse the repository at this point in the history
Initial Q-Chem input generator
  • Loading branch information
RaphaelRobidas committed Mar 28, 2023
2 parents f3d495f + a6aacab commit 5e18f47
Show file tree
Hide file tree
Showing 6 changed files with 2,165 additions and 40 deletions.
23 changes: 22 additions & 1 deletion ccinput/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ class CalcType(Enum):
"neb",
"nudgedelasticband",
"reactionpath",
"Freezing String Method",
"Freezing String",
"fsm",
],
CalcType.MO: [
"MO Calculation",
Expand Down Expand Up @@ -161,6 +164,7 @@ class CalcType(Enum):
SYN_SOFTWARE = {
"gaussian": ["g16", "gaussian16"],
"orca": ["orca5"],
"qchem": [],
"xtb": [],
}

Expand All @@ -169,7 +173,7 @@ class CalcType(Enum):
"acetonitrile": ["acetonitrile", "acn", "ch3cn", "mecn"],
"benzene": ["benzene", "c6h6"],
"dichloromethane": ["ch2cl2", "dcm"],
"chloroform": ["chcl3"],
"chloroform": ["chcl3", "trichloromethane"],
"carbondisulfide": ["cs2"],
"dimethylformamide": ["dmf"],
"dimethylsulfoxide": ["dmso"],
Expand Down Expand Up @@ -1590,6 +1594,13 @@ class CalcType(Enum):
"pm7": "PM7",
"hf": "HF",
},
"qchem": {
"am1": "am1",
"b3lyp": "b3lyp",
"hf": "hf",
"m062x": "m06-2x",
"pbe0": "pbe0",
},
"xtb": {
"gfn2xtb": "gfn2-xtb",
"gfn1xtb": "gfn1-xtb",
Expand Down Expand Up @@ -2028,6 +2039,12 @@ class CalcType(Enum):
"gdtzvp": "DGTZVP",
"cbsb7": "cbsb7",
},
"qchem": {
"321g": "3-21g",
"631+gdp": "6-31+g(d,p)",
"def2svp": "def2-svp",
"def2tzvp": "def2-tzvp",
},
}

SOFTWARE_SOLVENTS = {
Expand Down Expand Up @@ -2405,6 +2422,10 @@ class CalcType(Enum):
"oxylene": "o-xylene",
"pxylene": "p-xylene",
},
"qchem": {
"chloroform": "trichloromethane",
"noctanol": "octanol",
},
"xtb": {
"acetone": "acetone",
"acetonitrile": "acetonitrile",
Expand Down
281 changes: 281 additions & 0 deletions ccinput/packages/qchem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
from typing import Dict, List

from ccinput.calculation import Calculation
from ccinput.constants import (
CalcType,
ATOMIC_NUMBER,
LOWERCASE_ATOMIC_SYMBOLS,
THEORY_LEVELS,
)
from ccinput.utilities import clean_xyz, get_basis_set, get_method, get_solvent
from ccinput.exceptions import InvalidParameter

_CALC_TYPE_TO_JOBTYPE = {
CalcType.SP: "sp",
CalcType.OPT: "opt",
# CalcType.CONSTR_OPT: [],
CalcType.FREQ: "freq",
CalcType.TS: "ts",
CalcType.MEP: "fsm",
# Go through moprop rather than d-scf
CalcType.NMR: "sp",
CalcType.UVVIS: "sp",
CalcType.UVVIS_TDA: "sp",
# plots?
# CalcType.MO: [],
# does combined exist?
# CalcType.OPTFREQ: [],
}

_TEMPLATE = """$comment
{comment}
$end
$molecule
{charge} {multiplicity}
{xyz_structure}$end
$rem
{rem_lines}
$end
{other_blocks}
"""


class QChemCalculation:
def __init__(self, calc: Calculation) -> None:
self.calc = calc
self.other_blocks: List[str] = []
self.solvation_radii: Dict[int, float] = {}

self._rems: Dict[str, str] = {}

self.xyz_structure = ""
self.input_file = ""

self._rems["jobtype"] = _CALC_TYPE_TO_JOBTYPE[self.calc.type]
if self.calc.type == CalcType.NMR:
self._rems["moprop"] = "nmr"
self._rems["method"] = get_method(self.calc.parameters.method, "qchem")
if self._rems["method"] not in THEORY_LEVELS["semiempirical"]:
self._rems["basis"] = get_basis_set(self.calc.parameters.basis_set, "qchem")
else:
self._rems["semi_empirical"] = "true"
self._rems["mem_total"] = str(self.calc.mem)

if self.calc.parameters.d3:
self._rems["dft_d"] = "d3_zero"
elif self.calc.parameters.d3bj:
self._rems["dft_d"] = "d3_bj"

self.handle_specifications()

# self.handle_command()
self.handle_xyz()

self.parse_custom_solvation_radii()
self.handle_solvation()

self.create_input_file()

@property
def confirmed_specifications(self):
pass

def clean(self, s: str) -> str:
WHITELIST = set(
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/()=-,. "
)
return "".join([c for c in s if c in WHITELIST])

def add_option(self, key, option) -> None:
pass

def adds_option(self, key, options) -> None:
pass

def add_commands(self, commands) -> None:
pass

def handle_specifications(self) -> None:
_specifications = self.clean(self.calc.parameters.specifications.lower())

if _specifications != "":
sspecs = _specifications.split()
ind = 0
while ind < len(sspecs):
spec = sspecs[ind]
if spec == "--phirshfeld":
self._rems["hirshfeld"] = "true"
ind += 1

def handle_command(self):
ctype = self.calc.type
if ctype == CalcType.MEP:
raise RuntimeError
else:
raise RuntimeError

def parse_custom_basis_set(self, base_bs):
pass

def handle_custom_basis_sets(self) -> None:
if len(self.calc.parameters.custom_basis_sets) == 0:
return

unique_atoms = _get_unique_atoms(self.calc.xyz)

for el, bs_keyword in self.calc.parameters.custom_basis_sets.items():
pass

def handle_xyz(self) -> None:
lines = [i + "\n" for i in clean_xyz(self.calc.xyz).split("\n") if i != ""]
self.xyz_structure = "".join(lines)

def parse_custom_solvation_radii(self) -> None:
for radius in self.calc.parameters.custom_solvation_radii.split(";"):
if radius.strip() == "":
continue
sradius = radius.split("=")
if len(sradius) != 2:
raise InvalidParameter(
f"Invalid custom solvation radius specification: '{radius}': must follow the pattern '<atom1>=<radius1>;...'"
)

element, rad = sradius
if element not in LOWERCASE_ATOMIC_SYMBOLS:
raise InvalidParameter(
f"Invalid element in custom solvation radius specifications: '{element}'"
)

_element = ATOMIC_NUMBER[LOWERCASE_ATOMIC_SYMBOLS[element]]

try:
_rad = float(rad)
except ValueError:
raise InvalidParameter(
f"Invalid custom solvation radius for element {element}: '{rad}'"
)
self.solvation_radii[_element] = _rad

def get_radii_specs(self) -> str:
radii_specs = []
if self.solvation_radii:
radii_specs.append("$van_der_waals")
radii_specs.append("1")
for el, rad in self.solvation_radii.items():
radii_specs.append(f"{el} {rad:.2f}")
radii_specs.append("$end")
return "\n".join(radii_specs)

def handle_solvation(self) -> None:
if self.calc.parameters.solvent.lower() not in ["vacuum", ""]:
solvent_keyword = get_solvent(
self.calc.parameters.solvent,
self.calc.parameters.software,
solvation_model=self.calc.parameters.solvation_model,
)
model = self.calc.parameters.solvation_model
radii_set = self.calc.parameters.solvation_radii
custom_radii = self.calc.parameters.custom_solvation_radii

if radii_set not in _ALLOWED_RADII_SETS:
raise InvalidParameter(
"invalid argument for solvent radii type: "
f"{radii_set} not in {_ALLOWED_RADII_SETS}"
)

radii_specs = self.get_radii_specs()
model_block = []
solv_block = []

if model == "kirkwood":
raise NotImplementedError(
f"solvent_method = {model} not implemented yet"
)
elif "pcm" in model:
pcm_model = model
model = "pcm"
model_block = ["$pcm", f"theory {pcm_model}"]
if radii_set != "":
model_block.append(f"radii {radii_set}")
if radii_specs:
if radii_set not in ("", "read"):
raise InvalidParameter(
f"custom radii set but not requested to read: {radii_set}"
)
if radii_set == "":
model_block.append("radii read")
model_block.append("$end")
solv_block = ["$solvent", f"solventname {solvent_keyword}", "$end"]
elif model == "isosvp":
raise NotImplementedError(
f"solvent_method = {model} not implemented yet"
)
elif model == "cosmo":
raise NotImplementedError(
f"solvent_method = {model} not implemented yet"
)
elif model == "sm8":
raise NotImplementedError(
f"solvent_method = {model} not implemented yet"
)
elif model == "sm12":
raise NotImplementedError(
f"solvent_method = {model} not implemented yet"
)
elif model == "smd":
solv_block = ["$smx", f"solvent {solvent_keyword}", "$end"]
elif model == "chem_sol":
raise NotImplementedError(
f"solvent_method = {model} not implemented yet"
)
elif model == "peqs":
raise NotImplementedError(
f"solvent_method = {model} not implemented yet"
)
else:
raise InvalidParameter(
f"Invalid solvation model for Q-Chem: "
f"'{self.calc.parameters.solvation_model}'"
)

self._rems["solvent_method"] = model

if model_block:
self.other_blocks.append("")
self.other_blocks.extend(model_block)
self.other_blocks.append("")
self.other_blocks.extend(solv_block)
if radii_specs:
self.other_blocks.append("")
self.other_blocks.append(radii_specs)

def create_input_file(self) -> None:
# breakpoint()
self.input_file = _TEMPLATE.format(
comment=self.calc.header,
rem_lines="\n".join(f"{k} = {v}" for k, v in self._rems.items()),
charge=self.calc.charge,
multiplicity=self.calc.multiplicity,
xyz_structure=self.xyz_structure,
other_blocks="\n".join(self.other_blocks),
)

@property
def output(self):
return self.input_file


def _get_unique_atoms(xyz: str) -> List[str]:
unique_atoms = []
for line in xyz.split("\n"):
if line.strip() == "":
continue
a, *_ = line.split()
if a not in unique_atoms:
unique_atoms.append(a)
return unique_atoms


_ALLOWED_RADII_SETS = ("", "bondi", "ff", "read")

0 comments on commit 5e18f47

Please sign in to comment.