Skip to content
Open
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
![Static Badge](https://img.shields.io/badge/contributing-CLA-red)
![Static Badge](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.15688425-blue)
![Static Badge](https://img.shields.io/badge/release-2.0.0-%2300AEC3)
[![JCTC Paper](https://img.shields.io/badge/JCTC-10.1021%2Faces.jctc.5c02141-blue?logo=doi)](https://doi.org/10.1021/acs.jctc.5c02141)
[![JCTC Paper](https://img.shields.io/badge/JCTC-10.1021%2Facs.jctc.5c02141-blue?logo=doi)](https://doi.org/10.1021/acs.jctc.5c02141)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change listed in the PR description?


The ORCA Python Interface (OPI) is a Python library to create input and parse output of [ORCA](https://www.faccts.de/orca/). It is designed as an open source community effort to make ORCA calculations as accessible as possible and is consistently supported by [FACCTs](https://www.faccts.de/), the co-developers of the ORCA quantum chemistry program package. Note that OPI is first introduced with ORCA 6.1 and is not compatible with earlier versions.
OPI version 2.0 and upward requires ORCA 6.1.1 as minimal ORCA version.
Expand Down
1 change: 1 addition & 0 deletions examples/exmp001_scf/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def run_exmp001(
output = calc.get_output()
if not output.terminated_normally():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
print(output.error_message())
sys.exit(1)
# << END OF IF

Expand Down
7 changes: 6 additions & 1 deletion examples/exmp002_scf_ccsdt/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ def run_exmp002(
if not output.terminated_normally():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
sys.exit(1)
# << END OF IF
if not output.scf_converged():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
sys.exit(1)
if not output.cc_converged():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
sys.exit(1)

# > Parse JSON files
output.parse()
Expand Down
7 changes: 6 additions & 1 deletion examples/exmp014_led/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ def run_exmp014(
if not output.terminated_normally():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
sys.exit(1)
# << END OF IF
if not output.scf_converged():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
sys.exit(1)
if not output.cc_converged():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
sys.exit(1)

# > Parse JSON files
output.parse()
Expand Down
4 changes: 3 additions & 1 deletion examples/exmp028_nevpt2/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def run_exmp028(
if not output.terminated_normally():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
sys.exit(1)
# << END OF IF
if not output.casscf_converged():
print(f"ORCA calculation failed, see output file: {output.get_outfile()}")
sys.exit(1)

# > Parse JSON files
output.parse()
Expand Down
3 changes: 3 additions & 0 deletions src/opi/input/blocks/block_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ class BlockMethod(Block):
d3s8: float | None = None
d3a2: float | None = None

# > Number of CPSCF iterations
z_maxiter: int | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this change listed in the PR description. Please cross-check.


# > Options for Extopt
ProgExt: InputFilePath | None = None # Path to wrapper script
Ext_Params: str | None = None # Arbitrary optional command line arguments
Expand Down
44 changes: 44 additions & 0 deletions src/opi/output/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
from opi.output.cube import CubeOutput
from opi.output.gbw_suffix import GbwSuffix
from opi.output.grepper.recipes import (
get_error_message,
get_float_from_line,
get_lines_from_block,
has_casscf_converged,
has_cc_converged,
has_geometry_optimization_converged,
has_scf_converged,
has_terminated_normally,
Expand Down Expand Up @@ -672,6 +675,13 @@ def terminated_normally(self) -> bool:
except FileNotFoundError:
return False

def error_message(self) -> str | None:
outfile = self.get_outfile()
try:
return get_error_message(outfile)
except FileNotFoundError:
return "Output File Not Found"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add the expected path ;)


def scf_converged(self) -> bool:
"""
Determine if ORCA SCF converged, by looking for "SUCCESS" in the ".out" file.
Expand All @@ -689,6 +699,40 @@ def scf_converged(self) -> bool:
except FileNotFoundError:
return False

def casscf_converged(self) -> bool:
"""
Determine if ORCA CAS-SCF converged, by looking for "THE CAS-SCF GRADIENT HAS CONVERGED" in the ".out" file.
Check only if ORCA CAS-SCF was actually requested.
If the ".out" file does not exist, also return False.

Returns
-------
bool
True if string is found in ".out" file else False
"""
outfile = self.get_outfile()
try:
return has_casscf_converged(outfile)
except FileNotFoundError:
return False
Comment on lines +713 to +717
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines seem to develop into a repeating pattern. We should write a driver for has_xxx-function. Something like:

# > Function name just exemplary
def _has(self, has_func: Callable[[Path], bool]) -> bool:
        outfile = self.get_outfile()
        try:
            return has_func(outfile)
        except FileNotFoundError:
            return False

casscf_converged() would then look as follows:

def casscf_converged(self):
     return self._has(has_cc_converged)


def cc_converged(self) -> bool:
"""
Determine if ORCA coupled-cluster iterations converged, by looking for "The Coupled-Cluster iterations have converged" in the ".out" file.
Check only if CC was actually requested.
If the ".out" file does not exist, also return False.

Returns
-------
bool
True if string is found in ".out" file else False
"""
outfile = self.get_outfile()
try:
return has_cc_converged(outfile)
except FileNotFoundError:
return False

def geometry_optimization_converged(self) -> bool:
"""
Determine if ORCA geometry optimization converged, by looking for "HURRAY" in the ".out" file.
Expand Down
152 changes: 152 additions & 0 deletions src/opi/output/grepper/error_pattern.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# patterns.py
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not done consistently adding the filename in the first line as comment.
Either do it for all files or none (which I would prefer), but please don't establish such new conventions in some (random) files only.

import re
from typing import Callable

from opi.output.grepper.core import Grepper


class ErrorPattern:
"""
Represents an error pattern in the ORCA output file.
More complex error patterns derive from this class and override the extractor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
More complex error patterns derive from this class and override the extractor
More complex error patterns derive from this class and override the `extractor`.


Attributes
----------
grep_string: str
The string that is searched in the output file.
message: str
A human-readable error message of the given error pattern.
critical: bool, default = False
When the error is critical we will stop searching for further errors after finding it.
extractor: Callable[[Grepper], str] | None, default = None
Optional function for extracting more details from the matched line.

"""

def __init__(
self,
grep_string: str | None = None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just go with empty strings as default? I don't see the value of using None here.
Applies to grep_string as well as message.

message: str | None = None,
critical: bool = False,
extractor: Callable[[Grepper], str] | None = None,
) -> None:
if grep_string is not None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would check as follows: if not grep_string (also applies to if message ...)
E.g. why would one want grep for an empty string?

self.grep_string = grep_string
if message is not None:
self.message = message
if critical is not None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above it says critical is always boolean. So I don't get why you check for None here.

self.critical = critical
self.extractor = extractor

def match(self, grepper: Grepper) -> str | None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Again. I would stick with an empty string rather than None as return value.
    Also applies to the extract() method.
  2. Please add docstring. Especially in the base class. And also some comments please.

hit = grepper.search(self.grep_string, case_sensitive=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't see the point why we would first search for self.grep_string and later use the extractor which looks for the same string.
Can we just process the return value of the extractor to determine if the error pattern applies here?

if not hit:
return None
if self.extractor:
return self.extractor(grepper)
return self.extract(grepper) or self.message

def extract(self, grepper: Grepper) -> str | None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should have a docstring. Especially in the base class.

return None


class InvalidLineError(ErrorPattern):
"""
Triggered when ORCA encounters an invalid line in the input file.
This typically means a line does not start with a valid ORCA input
character such as '$', '!', '%', '*' or '['.
"""

grep_string = "ERROR: expect a '$', '!', '%', '*' or '[' in the input"
message = "Invalid input line in ORCA input"
critical = True

def extract(self, grepper: Grepper) -> str | None:
match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1)
if match:
m = re.search(r"\((.+?)\)", match[0])
result = m.group(1) if m else None
return f"Invalid line starting with: {result}" if result else None
return None


class SimpleKeywordsError(ErrorPattern):
"""
Triggered when ORCA encounters an unrecognized or duplicated keyword
in the simple input line (the '!' line).
"""

grep_string = "UNRECOGNIZED OR DUPLICATED KEYWORD(S) IN SIMPLE INPUT LINE"
message = "An unrecognized or duplicated simple keyword was requested"
critical = True

def extract(self, grepper: Grepper) -> str | None:
match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1)
return f"Unknown/duplicate simple keyword(s): {match[0]}" if match else None


class UnknownBlockError(ErrorPattern):
"""
Triggered when ORCA encounters an unknown block name in the input file,
i.e. a '%blockname' that ORCA does not recognize.
"""

grep_string = "Unknown identifier"
message = "An unknown block was requested"
critical = True

def extract(self, grepper: Grepper) -> str | None:
match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=0)
return f"Unknown block: {match[0].split()[-1]}" if match else None


class UnknownBlockKeyError(ErrorPattern):
"""
Triggered when ORCA encounters an unknown key inside a block,
i.e. a valid block name but an unrecognized option within it.
"""

grep_string = "Unknown identifier in"
message = "An unknown block option was requested"
critical = True

def extract(self, grepper: Grepper) -> str | None:
match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1)
return f"Unknown block key: {match[0].split(':')[-1]}" if match else None


class UnknownBlockValueError(ErrorPattern):
"""
Triggered when ORCA encounters an invalid value for a block option,
i.e. the key is recognized but the assigned value is not valid.
"""

grep_string = "Invalid assignment"
message = "An invalid value was requested in a block"
critical = True

def extract(self, grepper: Grepper) -> str | None:
match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1)
return f"Unknown block value: {match[0].split(':')[-1]}" if match else None


class NotEnoughMemoryScfError(ErrorPattern):
"""
Triggered when there is not enough memory available for the SCF
"""

grep_string = "Error (ORCA_SCF): Not enough memory available!"
message = "Not enough memory for SCF available"
critical = True

def extract(self, grepper: Grepper) -> str | None:
mem_avail = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1)[-1].split(
":"
)[-1]
mem_estimated = grepper.search(self.grep_string, case_sensitive=True, skip_lines=2)[
-1
].split(":")[-1]
if mem_estimated and mem_avail:
return f"Not enough memory available for SCF. Available: {mem_avail}, Required: {mem_estimated}"
else:
return None
80 changes: 80 additions & 0 deletions src/opi/output/grepper/patterns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from opi.output.grepper.error_pattern import (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add module level docstring, describing what this file holds.

ErrorPattern,
InvalidLineError,
NotEnoughMemoryScfError,
SimpleKeywordsError,
UnknownBlockError,
UnknownBlockKeyError,
UnknownBlockValueError,
)

# > Success strings
TERMINATED_NORMALLY = "****ORCA TERMINATED NORMALLY****"
SCF_CONVERGED = "SUCCESS"
GEOMETRY_CONVERGED = "HURRAY"
CC_CONVERGED = "The Coupled-Cluster iterations have converged"
CASSCF_CONVERGED = "---- THE CAS-SCF GRADIENT HAS CONVERGED ----"

# > Has strings
HAS_GEOMETRY_OPT = "Geometry Optimization Run"
HAS_SCF = "SCF SETTINGS"
HAS_ABORTING = "aborting"

# > Error patterns in order of priority.
# > Critical errors will stop scanning when matched.
# > Non-critical errors will just be added and reported.
ERROR_PATTERNS: list[ErrorPattern] = [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add kind of line in the form of a comment, at which point in the list we go from critical to non-critical.

# > Critical input errors - stop scanning on first match
InvalidLineError(),
SimpleKeywordsError(),
UnknownBlockValueError(),
UnknownBlockKeyError(),
UnknownBlockError(),
ErrorPattern(
"You must have a [COORDS] ... [END] block in your input",
"No coordinates in the ORCA input.",
critical=True,
),
# > Convergence errors
ErrorPattern(
"Error (SHARK/CP-SCF Solver): Unfortunately, the calculation did not converge.",
"CP-SCF did not converge",
critical=True,
),
ErrorPattern(
"The Coupled-Cluster iterations have NOT converged",
"Coupled-Cluster did not converge",
critical=True,
),
ErrorPattern("CIS/TDA-DFT did not converge", "CIS/TDA-DFT did not converge"),
ErrorPattern("SCF NOT CONVERGED", "SCF did not converge", critical=True),
ErrorPattern(
"The optimization did not converge",
"Geometry optimization did not converge",
critical=False,
),
# > Memory Errors
NotEnoughMemoryScfError(),
ErrorPattern(
"Error (ORCA_MDCI): not enough memory for computing triples",
"Not enough memory for triples calculation",
critical=True,
),
ErrorPattern("ERROR - OUT OF MEMORY !!!", "Calculation ran out of memory", critical=False),
# > Module terminates not normally
ErrorPattern(
"ORCA finished by error termination in MDCI",
"Error in MDCI part of the calculation",
critical=True,
),
ErrorPattern(
"ORCA finished by error termination in MP2",
"Error in MP2 part of the calculation",
critical=True,
),
# > Potentially MPI related error
ErrorPattern("-" * 74, "Potentially an Open MPI related error occurred.", critical=False),
# > Unspecific errors
ErrorPattern("ABORTING THE RUN", "ORCA aborted the run"),
ErrorPattern("ERROR", "ORCA encountered an error"),
]
Loading
Loading