Skip to content
19 changes: 19 additions & 0 deletions src/opi/input/blocks/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ def format_orca(self) -> str:

return s

def __add__(self, other: "Block") -> "Block":
new_block = self.__class__.model_validate(
{**self.model_dump(), **other.model_dump(exclude_unset=True)}
)
return new_block

@property
def name(self) -> str:
return self._name
Expand All @@ -189,3 +195,16 @@ def init_inputpath(cls, inp: Any) -> Any:
return InputFilePath(file=inp)
else:
return inp

@classmethod
def get_subclass_by_name(cls, name: str) -> type["Block"]:
matches = {sub.__name__.lower(): sub for sub in cls.__subclasses__()}
name_matches = {sub().name.lower(): sub for sub in cls.__subclasses__()}
match = matches.get(name.lower()) or name_matches.get(name.lower())

if match is None:
raise ValueError(
f"No Block subclass found with name {name!r}. Available: {list(matches.keys())}"
)

return match
2 changes: 1 addition & 1 deletion src/opi/input/blocks/block_ice.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ class BlockIce(Block):
etol: float | None = None
icetype: Literal["CFGs", "CSFs", "DETs"] | None = None
# > algorithm details
integrals: Literal["exact", "ri"] | None
integrals: Literal["exact", "ri"] | None = None
52 changes: 49 additions & 3 deletions src/opi/input/simple_keywords/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,52 @@
__all__ = ("SimpleKeyword",)
__all__ = ("SimpleKeyword", "SimpleKeywordBox")

from typing import Any


class SimpleKeywordBox:
pass
"""
TODO:
- rework registry to account for latest changes.
"""
_registry: list[type["SimpleKeywordBox"]] = []

def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
cls._registry = []

for base in cls.__bases__:
if hasattr(base, "_registry"):
base._registry.append(cls)

cls._registry.append(cls)

@classmethod
def registry(cls) -> list[type["SimpleKeywordBox"]]:
return cls._registry

@classmethod
def from_string(cls, s: str) -> "SimpleKeyword":
norm = s.lower()
for c in cls._registry:
for attr in dir(c):
if attr.startswith('_'): # Skip private/magic attributes
continue
value = getattr(c, attr)
if isinstance(value, SimpleKeyword) and value.keyword.lower() == norm:
return value
elif isinstance(value, SimpleKeyword) and attr.lower() == norm:
return value
elif isinstance(value, SimpleKeyword) and value.alias and value.alias.lower() == norm:
return value

raise ValueError(f"Keyword {s} not found in class {cls.__name__}")

@classmethod
def find_keyword(cls, inp: "SimpleKeyword | str") -> "SimpleKeyword":
if isinstance(inp, SimpleKeyword):
inp = inp.keyword

return cls.from_string(inp)


class SimpleKeyword:
Expand All @@ -17,12 +61,14 @@ class SimpleKeyword:
keyword: str
simple keyword as it will appear in the ORCA .inp file
"""
alias: str | None = None

def __init__(self, keyword: str) -> None:
def __init__(self, keyword: str, alias:str|None=None) -> None:
self._keyword: str = ""
self.keyword = keyword
self._name: str = ""
# self.name = name
self.alias = alias

@property
def keyword(self) -> str:
Expand Down
6 changes: 3 additions & 3 deletions src/opi/input/simple_keywords/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
class Grid(SimpleKeywordBox):
"""Enum to store all simple keywords of type Grid."""

DEFGRID1 = SimpleKeyword("defgrid1")
DEFGRID1 = SimpleKeyword("defgrid1", alias="1")
"""SimpleKeyword: small grid."""
DEFGRID2 = SimpleKeyword("defgrid2")
DEFGRID2 = SimpleKeyword("defgrid2", alias="2")
"""SimpleKeyword: medium grid."""
DEFGRID3 = SimpleKeyword("defgrid3")
DEFGRID3 = SimpleKeyword("defgrid3", alias="3")
"""SimpleKeyword: large grid."""
REFGRID = SimpleKeyword("refgrid")
"""SimpleKeyword: reference grid."""
Expand Down
27 changes: 15 additions & 12 deletions src/opi/input/simple_keywords/opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@
__all__ = ("Opt",)


class Opt(SimpleKeywordBox):
"""Enum to store all simple keywords of type Opt."""

OPT = SimpleKeyword("opt")
"""SimpleKeyword: Perform a geometry optimization."""
CRUDEOPT = SimpleKeyword("crudeopt")
class OptThreshold(SimpleKeywordBox):
CRUDEOPT = SimpleKeyword("crudeopt", alias="crude")
"""SimpleKeyword: Geometry optimization with thresholds."""
INTERPOPT = SimpleKeyword("interpopt")
LOOSEOPT = SimpleKeyword("looseopt", alias="loose")
"""SimpleKeyword: Geometry optimization with thresholds."""
LOOSEOPT = SimpleKeyword("looseopt")
NORMALOPT = SimpleKeyword("normalopt", alias="normal")
"""SimpleKeyword: Geometry optimization with thresholds."""
NORMALOPT = SimpleKeyword("normalopt")
SLOPPYOPT = SimpleKeyword("sloppyopt", alias="sloppy")
"""SimpleKeyword: Geometry optimization with thresholds."""
SLOPPYOPT = SimpleKeyword("sloppyopt")
TIGHTOPT = SimpleKeyword("tightopt", alias="tight")
"""SimpleKeyword: Geometry optimization with thresholds."""
TIGHTOPT = SimpleKeyword("tightopt")
VERYTIGHTOPT = SimpleKeyword("verytightopt", alias="verytight")
"""SimpleKeyword: Geometry optimization with thresholds."""
VERYTIGHTOPT = SimpleKeyword("verytightopt")


class Opt(SimpleKeywordBox):
"""Enum to store all simple keywords of type Opt."""

OPT = SimpleKeyword("opt")
"""SimpleKeyword: Perform a geometry optimization."""
INTERPOPT = SimpleKeyword("interpopt")
"""SimpleKeyword: Geometry optimization with thresholds."""
OPTH = SimpleKeyword("opth")
"""SimpleKeyword: Optimize only hydrogen atoms."""
Expand Down
114 changes: 65 additions & 49 deletions src/opi/input/simple_keywords/scf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,67 @@

__all__ = ("Scf",)

class ScfThreshold(SimpleKeywordBox):
SLOPPYSCF = SimpleKeyword("sloppyscf", alias="sloppy")
"""SimpleKeyword: SCF convergence threshold settings."""
LOOSESCF = SimpleKeyword("loosescf", alias="loose")
"""SimpleKeyword: SCF convergence threshold settings."""
NORMALSCF = SimpleKeyword("normalscf", alias="normal")
"""SimpleKeyword: SCF convergence threshold settings."""
STRONGSCF = SimpleKeyword("strongscf", alias="strong")
"""SimpleKeyword: SCF convergence threshold settings."""
TIGHTSCF = SimpleKeyword("tightscf", alias="tight")
"""SimpleKeyword: SCF convergence threshold settings."""
VERYTIGHTSCF = SimpleKeyword("verytightscf", alias="verytight")
"""SimpleKeyword: SCF convergence threshold settings."""
EXTREMESCF = SimpleKeyword("extremescf", alias="extreme")
"""SimpleKeyword: SCF convergence threshold settings."""


class ScfGuess(SimpleKeywordBox):
MOREAD = SimpleKeyword("moread")
"""SimpleKeyword: SCF initial guess read orbitals from gbw file."""
EHTANO = SimpleKeyword("ehtano")
"""SimpleKeyword: SCF initial guess."""
HCORE = SimpleKeyword("hcore")
"""SimpleKeyword: SCF initial guess."""
PATOM = SimpleKeyword("patom")
"""SimpleKeyword: SCF initial guess."""
PMODEL = SimpleKeyword("pmodel")
"""SimpleKeyword: SCF initial guess."""
PMODELX = SimpleKeyword("pmodelx")
"""SimpleKeyword: SCF initial guess."""
PMODELXAV = SimpleKeyword("pmodelxav")
"""SimpleKeyword: SCF initial guess."""
PMODELXPM = SimpleKeyword("pmodelxpm")
"""SimpleKeyword: SCF initial guess."""
SYMBREAKGUESS = SimpleKeyword("symbreakguess")
"""SimpleKeyword: SCF initial guess."""


class ScfConvergence(SimpleKeywordBox):
EASYCONV = SimpleKeyword("easyconv", alias="easy")
"""SimpleKeyword: SCF convergence strategy."""
NORMALCONV = SimpleKeyword("normalconv", alias="normal")
"""SimpleKeyword: SCF convergence strategy."""
SLOWCONV = SimpleKeyword("slowconv", alias="slow")
"""SimpleKeyword: SCF convergence strategy."""
VERYSLOWCONV = SimpleKeyword("veryslowconv", alias="veryslow")
"""SimpleKeyword: SCF convergence strategy."""


class ScfSolver(SimpleKeywordBox):
DIIS = SimpleKeyword("diis")
"""SimpleKeyword: SCF solver."""
KDIIS = SimpleKeyword("kdiis")
"""SimpleKeyword: SCF solver."""
SOSCF = SimpleKeyword("soscf")
"""SimpleKeyword: SCF solver."""
TRAH = SimpleKeyword("trah")
"""SimpleKeyword: SCF solver."""

class Scf(SimpleKeywordBox):

class Scf(ScfThreshold, ScfConvergence, ScfGuess, ScfSolver):
"""Enum to store all simple keywords of type Scf."""

G3CONV = SimpleKeyword("3conv")
Expand All @@ -17,50 +76,29 @@ class Scf(SimpleKeywordBox):
"""SimpleKeyword: SCF solver combination."""
KDIISTRAH = SimpleKeyword("kdiistrah")
"""SimpleKeyword: SCF solver combination."""
DIIS = SimpleKeyword("diis")
"""SimpleKeyword: SCF solver."""

NODIIS = SimpleKeyword("nodiis")
"""SimpleKeyword: SCF solver."""
AODIIS = SimpleKeyword("aodiis")
"""SimpleKeyword: SCF solver."""
NOAODIIS = SimpleKeyword("noaodiis")
"""SimpleKeyword: SCF solver."""
KDIIS = SimpleKeyword("kdiis")
"""SimpleKeyword: SCF solver."""

NOKDIIS = SimpleKeyword("nokdiis")
"""SimpleKeyword: SCF solver."""
SOSCF = SimpleKeyword("soscf")
"""SimpleKeyword: SCF solver."""

NOSOSCF = SimpleKeyword("nososcf")
"""SimpleKeyword: SCF solver."""
TRAH = SimpleKeyword("trah")
"""SimpleKeyword: SCF solver."""

NOTRAH = SimpleKeyword("notrah")
"""SimpleKeyword: SCF solver."""
AUTOSTART = SimpleKeyword("autostart")
"""SimpleKeyword: SCF initial guess start SCF from a gbw file with the same basename (default)."""
NOAUTOSTART = SimpleKeyword("noautostart")
"""SimpleKeyword: SCF initial guess do not start SCF from a gbw file with the same basename."""
MOREAD = SimpleKeyword("moread")
"""SimpleKeyword: SCF initial guess read orbitals from gbw file."""
EHTANO = SimpleKeyword("ehtano")
"""SimpleKeyword: SCF initial guess."""
HCORE = SimpleKeyword("hcore")
"""SimpleKeyword: SCF initial guess."""
HUECKEL = SimpleKeyword("hueckel")
"""SimpleKeyword: SCF initial guess."""
PATOM = SimpleKeyword("patom")
"""SimpleKeyword: SCF initial guess."""
PMODEL = SimpleKeyword("pmodel")
"""SimpleKeyword: SCF initial guess."""
PMODELX = SimpleKeyword("pmodelx")
"""SimpleKeyword: SCF initial guess."""
PMODELXAV = SimpleKeyword("pmodelxav")
"""SimpleKeyword: SCF initial guess."""
PMODELXPM = SimpleKeyword("pmodelxpm")
"""SimpleKeyword: SCF initial guess."""
SYMBREAKGUESS = SimpleKeyword("symbreakguess")
"""SimpleKeyword: SCF initial guess."""

UNITMATRIXGUESS = SimpleKeyword("unitmatrixguess")
"""SimpleKeyword: SCF initial guess."""
USEGRAMSCHMIDT = SimpleKeyword("usegramschmidt")
Expand Down Expand Up @@ -91,20 +129,6 @@ class Scf(SimpleKeywordBox):
"""SimpleKeyword: enable fractional occupations."""
SCFCONVFORCED = SimpleKeyword("scfconvforced")
"""SimpleKeyword: Force SCF convergence for subsequent operations."""
SLOPPYSCF = SimpleKeyword("sloppyscf")
"""SimpleKeyword: SCF convergence threshold settings."""
LOOSESCF = SimpleKeyword("loosescf")
"""SimpleKeyword: SCF convergence threshold settings."""
NORMALSCF = SimpleKeyword("normalscf")
"""SimpleKeyword: SCF convergence threshold settings."""
STRONGSCF = SimpleKeyword("strongscf")
"""SimpleKeyword: SCF convergence threshold settings."""
TIGHTSCF = SimpleKeyword("tightscf")
"""SimpleKeyword: SCF convergence threshold settings."""
VERYTIGHTSCF = SimpleKeyword("verytightscf")
"""SimpleKeyword: SCF convergence threshold settings."""
EXTREMESCF = SimpleKeyword("extremescf")
"""SimpleKeyword: SCF convergence threshold settings."""
SLOPPYSCFCHECK = SimpleKeyword("sloppyscfcheck")
"""SimpleKeyword: SCF convergence threshold settings."""
NOSLOPPYSCFCHECK = SimpleKeyword("nosloppyscfcheck")
Expand All @@ -125,14 +149,6 @@ class Scf(SimpleKeywordBox):
"""SimpleKeyword: SCF convergence threshold settings."""
SCFCONV12 = SimpleKeyword("scfconv12")
"""SimpleKeyword: SCF convergence threshold settings."""
EASYCONV = SimpleKeyword("easyconv")
"""SimpleKeyword: SCF convergence strategy."""
NORMALCONV = SimpleKeyword("normalconv")
"""SimpleKeyword: SCF convergence strategy."""
SLOWCONV = SimpleKeyword("slowconv")
"""SimpleKeyword: SCF convergence strategy."""
VERYSLOWCONV = SimpleKeyword("veryslowconv")
"""SimpleKeyword: SCF convergence strategy."""
DAMP = SimpleKeyword("damp")
"""SimpleKeyword: SCF settings."""
NODAMP = SimpleKeyword("nodamp")
Expand Down
2 changes: 1 addition & 1 deletion src/opi/input/simple_keywords/solvation_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def __init__(self, model: str) -> None:
super().__init__(model)

def __call__(self, solvent: Solvent, /) -> SimpleKeyword:
if not isinstance(solvent, Solvent):
if not isinstance(solvent, Solvent | str):
raise TypeError(f"Solvent '{solvent}' is not of Solvent type!")
return SimpleKeyword(f"{self.keyword}({solvent})")

Expand Down
8 changes: 8 additions & 0 deletions src/opi/input/simple_keywords/solvent.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,11 @@ class Solvent(StrEnum):
WOCTANOL = "WOCTANOL"
WETOCTANOL = "WETOCTANOL"
CONDUCTOR = "CONDUCTOR"

@classmethod
def find_keyword(cls, key: str) -> str:
norm = key.lower()
for member in cls:
if member.value.lower() == norm or member.name.lower() == norm:
return str(member.value)
raise ValueError(f"Key {key} not found in {cls.__name__}")
53 changes: 53 additions & 0 deletions src/opi/tasks/engrad_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import typing

from opi.input.simple_keywords import Task, SimpleKeyword, Solvent
from opi.tasks.method_settings import DFTSettings
from opi.tasks.task_base import SimpleTask, TaskSettings, TaskResults


class EngradSettings(TaskSettings):
_name: str = "engrad"
task_keyword: typing.Annotated[SimpleKeyword, Task] = Task.ENGRAD


class EngradTask(SimpleTask):
def __init__(self,
method: str | SimpleKeyword,
basis_set: str | SimpleKeyword | None = None,
solvation_model: str | SimpleKeyword | None = None,
solvent: str | Solvent | None = None,
task: str | SimpleKeyword | None = None):
self._method_settings = DFTSettings(
method=method, basis_set=basis_set, solvation_model=solvation_model, solvent=solvent
)
self._task_settings = (
EngradSettings(task_keyword=task)
) if task else EngradSettings()

self._results_type = EngradResults


class EngradResults(TaskResults):
@property
@TaskResults.output_parse
def final_energy(self) -> float:
final_energy = self.output.get_final_energy()

if final_energy is None:
raise ValueError("Could not get final energy from ORCA Output")

return final_energy

@property
@TaskResults.output_parse
def gradient(self) -> list[float]:
gradient = self.output.get_gradient()

if gradient is None:
raise ValueError("Could not get gradient from ORCA Output")

return gradient

@property
def primary_property(self) -> tuple[float, list[float]]:
return self.final_energy, self.gradient
Loading