From b4665b89d52ce47a5259376535efff70a4dda42c Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:52:23 +0100 Subject: [PATCH 01/14] Minimal implementation of error_message --- src/opi/output/core.py | 8 +++++ src/opi/output/grepper/error_pattern.py | 29 +++++++++++++++ src/opi/output/grepper/patterns.py | 35 ++++++++++++++++++ src/opi/output/grepper/recipes.py | 47 +++++++++++++++++++++---- 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 src/opi/output/grepper/error_pattern.py create mode 100644 src/opi/output/grepper/patterns.py diff --git a/src/opi/output/core.py b/src/opi/output/core.py index db542984..4ecf2e3b 100644 --- a/src/opi/output/core.py +++ b/src/opi/output/core.py @@ -18,6 +18,7 @@ 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_geometry_optimization_converged, @@ -672,6 +673,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" + def scf_converged(self) -> bool: """ Determine if ORCA SCF converged, by looking for "SUCCESS" in the ".out" file. diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py new file mode 100644 index 00000000..88ea968f --- /dev/null +++ b/src/opi/output/grepper/error_pattern.py @@ -0,0 +1,29 @@ +# patterns.py +from dataclasses import dataclass +from typing import Callable + + +@dataclass +class ErrorPattern: + """ + Represents an error pattern in the ORCA output file. + + 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. + extractor: Callable | None, default = None + Optional function for extracting more details from the matched line + + """ + + grep_string: str + message: str + extractor: Callable[[str], str] | None = None + +# > extractor functions for more elaborate error messages +def _extract_keyword(line: str) -> str: + # e.g. parse "Unknown keyword: BLYPP" from the matched line + return f"Unknown/duplicate keyword: {line.split()[-1].strip()}" diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py new file mode 100644 index 00000000..5ee65712 --- /dev/null +++ b/src/opi/output/grepper/patterns.py @@ -0,0 +1,35 @@ +from opi.output.grepper.error_pattern import ErrorPattern, _extract_keyword + +# > Success strings +TERMINATED_NORMALLY = "****ORCA TERMINATED NORMALLY****" +SCF_CONVERGED = "SUCCESS" +GEOMETRY_CONVERGED = "HURRAY" +CC_CONVERGED = "The Coupled-Cluster iterations have converged" + +# > Has strings +HAS_GEOMETRY_OPT = "Geometry Optimization Run" +HAS_SCF = "SCF SETTINGS" +HAS_ABORTING = "aborting" + +# > List of known error patterns +# > In decreasing order of priority +ERROR_PATTERNS: list[ErrorPattern] = [ + ErrorPattern( + "ERROR: expect a '$', '!', '%', '*' or '[' in the input", "Invalid input line in ORCA input" + ), + ErrorPattern( + "UNRECOGNIZED OR DUPLICATED KEYWORD(S) IN SIMPLE INPUT LINE", + "An unrecognized or duplicated simple keyword was requested", + extractor=_extract_keyword, + ), + ErrorPattern("Unknown identifier in", "An unknown block option was requested"), + ErrorPattern("Invalid assignment", "An invalid value was requested in a block"), + ErrorPattern( + "The Coupled-Cluster iterations have NOT converged", "Coupled-Cluster did not converge" + ), + ErrorPattern("CIS/TDA-DFT did not converge", "CIS/TDA-DFT did not converge"), + ErrorPattern("SCF NOT CONVERGED", "SCF did not converge"), + ErrorPattern("The optimization did not converge", "Geometry optimization did not converge"), + ErrorPattern("ABORTING THE RUN", "ORCA aborted the run"), + ErrorPattern("ERROR", "ORCA encountered an error"), +] diff --git a/src/opi/output/grepper/recipes.py b/src/opi/output/grepper/recipes.py index acb8f10d..591fc1f0 100644 --- a/src/opi/output/grepper/recipes.py +++ b/src/opi/output/grepper/recipes.py @@ -1,6 +1,41 @@ from pathlib import Path from opi.output.grepper.core import Grepper +from opi.output.grepper.patterns import ( + ERROR_PATTERNS, + GEOMETRY_CONVERGED, + HAS_ABORTING, + HAS_GEOMETRY_OPT, + HAS_SCF, + SCF_CONVERGED, + TERMINATED_NORMALLY, +) + + +def get_error_messages(file_name: Path) -> list[str] | None: + """Return all matched error messages, or None if none found""" + grepper = Grepper(file_name) + hits: list[str] = [] + for pattern in ERROR_PATTERNS: + match = grepper.search(pattern.grep_string, case_sensitive=True) + if match: + if pattern.extractor: + # > If an extractor function is defined we get additional context and call the extractor + context_line = grepper.search( + pattern.grep_string, case_sensitive=True, skip_lines=1 + ) + detail = pattern.extractor(context_line[-1]) + else: + # > If no extractor function is defined we just print the designated message + detail = pattern.message + hits.append(detail) + return hits if hits else None + + +def get_error_message(file_name: Path) -> str | None: + """Return the most important matched error message, or None if none found.""" + messages = get_error_messages(file_name) + return next(iter(messages or []), None) def has_string_in_file(file_name: Path, search_for: str, /, *, strict: bool = True) -> bool: @@ -160,7 +195,7 @@ def has_terminated_normally(file_name: Path, /) -> bool: bool: True if string is present, else False. """ - return has_string_in_file(file_name, "****ORCA TERMINATED NORMALLY****") + return has_string_in_file(file_name, TERMINATED_NORMALLY) def has_aborted_run(file_name: Path, /) -> bool: @@ -176,7 +211,7 @@ def has_aborted_run(file_name: Path, /) -> bool: ------- bool """ - return has_string_in_file(file_name, "aborting") + return has_string_in_file(file_name, HAS_ABORTING) def has_geometry_optimization(file_name: Path, /) -> bool: @@ -193,7 +228,7 @@ def has_geometry_optimization(file_name: Path, /) -> bool: bool True if expression is found in file else False """ - return has_string_in_file(file_name, "Geometry Optimization Run") + return has_string_in_file(file_name, HAS_GEOMETRY_OPT) def has_geometry_optimization_converged(file_name: Path, /) -> bool: @@ -210,7 +245,7 @@ def has_geometry_optimization_converged(file_name: Path, /) -> bool: bool True if expression is found in file else False """ - return has_string_in_file(file_name, "HURRAY") + return has_string_in_file(file_name, GEOMETRY_CONVERGED) def has_scf(file_name: Path, /) -> bool: @@ -227,7 +262,7 @@ def has_scf(file_name: Path, /) -> bool: bool True if expression is found in file else False """ - return has_string_in_file(file_name, "SCF SETTINGS") + return has_string_in_file(file_name, HAS_SCF) def has_scf_converged(file_name: Path, /) -> bool: @@ -244,4 +279,4 @@ def has_scf_converged(file_name: Path, /) -> bool: bool True if expression is found in file else False """ - return has_string_in_file(file_name, "SUCCESS") + return has_string_in_file(file_name, SCF_CONVERGED) From 002c1539ccfd702407cd864254f65f3315c26139 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:12:46 +0100 Subject: [PATCH 02/14] fix nox issue --- src/opi/output/grepper/error_pattern.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index 88ea968f..7523721b 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -23,6 +23,7 @@ class ErrorPattern: message: str extractor: Callable[[str], str] | None = None + # > extractor functions for more elaborate error messages def _extract_keyword(line: str) -> str: # e.g. parse "Unknown keyword: BLYPP" from the matched line From 1eaf2ea20e39aa53be1c6e4c9eaafebf80888545 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:16:40 +0100 Subject: [PATCH 03/14] Small update to unkown keyword --- examples/exmp001_scf/job.py | 1 + src/opi/output/grepper/error_pattern.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/exmp001_scf/job.py b/examples/exmp001_scf/job.py index 380ce59d..f3edeceb 100755 --- a/examples/exmp001_scf/job.py +++ b/examples/exmp001_scf/job.py @@ -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 diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index 7523721b..95d57ac6 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -27,4 +27,4 @@ class ErrorPattern: # > extractor functions for more elaborate error messages def _extract_keyword(line: str) -> str: # e.g. parse "Unknown keyword: BLYPP" from the matched line - return f"Unknown/duplicate keyword: {line.split()[-1].strip()}" + return f"Unknown/duplicate keyword(s): {line.strip()}" From d7e0dba7c39dee82996466bddb43db854d93ac09 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:43:50 +0100 Subject: [PATCH 04/14] Simplekeywords, block key and value --- src/opi/output/grepper/error_pattern.py | 19 +++++++++++++++---- src/opi/output/grepper/patterns.py | 9 ++++++--- src/opi/output/grepper/recipes.py | 5 +---- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index 95d57ac6..045d321b 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from typing import Callable +from opi.output.grepper.core import Grepper + @dataclass class ErrorPattern: @@ -21,10 +23,19 @@ class ErrorPattern: grep_string: str message: str - extractor: Callable[[str], str] | None = None + extractor: Callable[[str, Grepper], str] | None = None # > extractor functions for more elaborate error messages -def _extract_keyword(line: str) -> str: - # e.g. parse "Unknown keyword: BLYPP" from the matched line - return f"Unknown/duplicate keyword(s): {line.strip()}" + + +def _simple_keywords(grep_string: str, grepper: Grepper) -> str: + """parse "Unknown keyword: BLYPP" from the matched line""" + match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) + return f"Unknown/duplicate keyword(s): {match[0]}" if match else "" + + +def _unknown_block(grep_string: str, grepper: Grepper) -> str: + """""" + match = grepper.search(grep_string, case_sensitive=True, skip_lines=0) + return f"Unknown Block: {match[0].split()[-1]}" if match else "" diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index 5ee65712..2a32a847 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -1,4 +1,4 @@ -from opi.output.grepper.error_pattern import ErrorPattern, _extract_keyword +from opi.output.grepper.error_pattern import ErrorPattern, _simple_keywords, _unknown_block # > Success strings TERMINATED_NORMALLY = "****ORCA TERMINATED NORMALLY****" @@ -20,9 +20,12 @@ ErrorPattern( "UNRECOGNIZED OR DUPLICATED KEYWORD(S) IN SIMPLE INPUT LINE", "An unrecognized or duplicated simple keyword was requested", - extractor=_extract_keyword, + extractor=_simple_keywords, + ), + ErrorPattern("Unknown identifier", "An unknown block was requested", extractor=_unknown_block), + ErrorPattern( + "Unknown identifier in", "An unknown block option was requested", extractor=_unknown_block ), - ErrorPattern("Unknown identifier in", "An unknown block option was requested"), ErrorPattern("Invalid assignment", "An invalid value was requested in a block"), ErrorPattern( "The Coupled-Cluster iterations have NOT converged", "Coupled-Cluster did not converge" diff --git a/src/opi/output/grepper/recipes.py b/src/opi/output/grepper/recipes.py index 591fc1f0..2f34c8ba 100644 --- a/src/opi/output/grepper/recipes.py +++ b/src/opi/output/grepper/recipes.py @@ -21,10 +21,7 @@ def get_error_messages(file_name: Path) -> list[str] | None: if match: if pattern.extractor: # > If an extractor function is defined we get additional context and call the extractor - context_line = grepper.search( - pattern.grep_string, case_sensitive=True, skip_lines=1 - ) - detail = pattern.extractor(context_line[-1]) + detail = pattern.extractor(pattern.grep_string, grepper) else: # > If no extractor function is defined we just print the designated message detail = pattern.message From 5d69098c288d97cd5935b39311121e41a26a5894 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:45:01 +0200 Subject: [PATCH 05/14] Initial failures input --- .../001_unknown_simple_keyword/job.py | 69 ++++++++++++++++++ examples/failures/002_unknown_block/job.py | 68 ++++++++++++++++++ .../failures/003_unknown_block_key/job.py | 70 ++++++++++++++++++ .../failures/004_unknown_block_value/job.py | 72 +++++++++++++++++++ src/opi/output/grepper/error_pattern.py | 14 +++- src/opi/output/grepper/patterns.py | 8 +-- 6 files changed, 295 insertions(+), 6 deletions(-) create mode 100755 examples/failures/001_unknown_simple_keyword/job.py create mode 100755 examples/failures/002_unknown_block/job.py create mode 100755 examples/failures/003_unknown_block_key/job.py create mode 100755 examples/failures/004_unknown_block_value/job.py diff --git a/examples/failures/001_unknown_simple_keyword/job.py b/examples/failures/001_unknown_simple_keyword/job.py new file mode 100755 index 00000000..e324ee5c --- /dev/null +++ b/examples/failures/001_unknown_simple_keyword/job.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import shutil +import sys +from pathlib import Path + +from opi.core import Calculator +from opi.input.blocks import BlockScf +from opi.input.simple_keywords import BasisSet, Method, Scf, Task +from opi.input.structures import Structure +from opi.output.core import Output + + +def unknown_simple_keyword( + structure: Structure | None = None, working_dir: Path | None = Path("RUN") +) -> Output: + # > recreate the working dir + shutil.rmtree(working_dir, ignore_errors=True) + working_dir.mkdir() + + # > if no structure is given take a smiles + if structure is None: + structure = Structure.from_smiles("O") + + # > set up the calculator + calc = Calculator(basename="job", working_dir=working_dir) + calc.structure = structure + calc.input.add_simple_keywords( + Scf.NOAUTOSTART, + Method.HF, + BasisSet.DEF2_SVP, + Task.SP, + ) + + calc.input.add_arbitrary_string("!hf hf hf") + # calc.input.add_arbitrary_string("%novalidblock") + + # > write the input and run the calculation + calc.write_input() + calc.run() + + # > get the output and check some results + 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 + + # > Parse JSON files + output.parse() + + # check for convergence of the SCF + if output.results_properties.geometries[0].single_point_data.converged: + print("SCF CONVERGED") + else: + print("SCF DID NOT CONVERGE") + sys.exit(1) + + print("FINAL SINGLE POINT ENERGY") + print(output.get_final_energy()) + # > is equal to + print(output.results_properties.geometries[-1].single_point_data.finalenergy) + + return output + + +if __name__ == "__main__": + output = unknown_simple_keyword() diff --git a/examples/failures/002_unknown_block/job.py b/examples/failures/002_unknown_block/job.py new file mode 100755 index 00000000..ad3e3c75 --- /dev/null +++ b/examples/failures/002_unknown_block/job.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +import shutil +import sys +from pathlib import Path + +from opi.core import Calculator +from opi.input.blocks import BlockScf +from opi.input.simple_keywords import BasisSet, Method, Scf, Task +from opi.input.structures import Structure +from opi.output.core import Output + + +def unknown_block( + structure: Structure | None = None, working_dir: Path | None = Path("RUN") +) -> Output: + # > recreate the working dir + shutil.rmtree(working_dir, ignore_errors=True) + working_dir.mkdir() + + # > if no structure is given take a smiles + if structure is None: + structure = Structure.from_smiles("O") + + # > set up the calculator + calc = Calculator(basename="job", working_dir=working_dir) + calc.structure = structure + calc.input.add_simple_keywords( + Scf.NOAUTOSTART, + Method.HF, + BasisSet.DEF2_SVP, + Task.SP, + ) + + calc.input.add_arbitrary_string("%novalidblock") + + # > write the input and run the calculation + calc.write_input() + calc.run() + + # > get the output and check some results + 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 + + # > Parse JSON files + output.parse() + + # check for convergence of the SCF + if output.results_properties.geometries[0].single_point_data.converged: + print("SCF CONVERGED") + else: + print("SCF DID NOT CONVERGE") + sys.exit(1) + + print("FINAL SINGLE POINT ENERGY") + print(output.get_final_energy()) + # > is equal to + print(output.results_properties.geometries[-1].single_point_data.finalenergy) + + return output + + +if __name__ == "__main__": + output = unknown_block() diff --git a/examples/failures/003_unknown_block_key/job.py b/examples/failures/003_unknown_block_key/job.py new file mode 100755 index 00000000..7367a79b --- /dev/null +++ b/examples/failures/003_unknown_block_key/job.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import shutil +import sys +from pathlib import Path + +from opi.core import Calculator +from opi.input.blocks import BlockScf +from opi.input.simple_keywords import BasisSet, Method, Scf, Task +from opi.input.structures import Structure +from opi.output.core import Output + + +def unknown_block_key( + structure: Structure | None = None, working_dir: Path | None = Path("RUN") +) -> Output: + # > recreate the working dir + shutil.rmtree(working_dir, ignore_errors=True) + working_dir.mkdir() + + # > if no structure is given take a smiles + if structure is None: + structure = Structure.from_smiles("O") + + # > set up the calculator + calc = Calculator(basename="job", working_dir=working_dir) + calc.structure = structure + calc.input.add_simple_keywords( + Scf.NOAUTOSTART, + Method.HF, + BasisSet.DEF2_SVP, + Task.SP, + ) + + scf_block = BlockScf() + scf_block.add_option(name="invalid_key", val="none") + calc.input.add_blocks(scf_block) + + # > write the input and run the calculation + calc.write_input() + calc.run() + + # > get the output and check some results + 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 + + # > Parse JSON files + output.parse() + + # check for convergence of the SCF + if output.results_properties.geometries[0].single_point_data.converged: + print("SCF CONVERGED") + else: + print("SCF DID NOT CONVERGE") + sys.exit(1) + + print("FINAL SINGLE POINT ENERGY") + print(output.get_final_energy()) + # > is equal to + print(output.results_properties.geometries[-1].single_point_data.finalenergy) + + return output + + +if __name__ == "__main__": + output = unknown_block_key() diff --git a/examples/failures/004_unknown_block_value/job.py b/examples/failures/004_unknown_block_value/job.py new file mode 100755 index 00000000..00fe3533 --- /dev/null +++ b/examples/failures/004_unknown_block_value/job.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +import shutil +import sys +from pathlib import Path + +from opi.core import Calculator +from opi.input.blocks import BlockScf +from opi.input.simple_keywords import BasisSet, Method, Scf, Task +from opi.input.structures import Structure +from opi.output.core import Output + + +def unknown_block_value( + structure: Structure | None = None, working_dir: Path | None = Path("RUN") +) -> Output: + # > recreate the working dir + shutil.rmtree(working_dir, ignore_errors=True) + working_dir.mkdir() + + # > if no structure is given take a smiles + if structure is None: + structure = Structure.from_smiles("O") + + # > set up the calculator + calc = Calculator(basename="job", working_dir=working_dir) + calc.structure = structure + calc.input.add_simple_keywords( + Scf.NOAUTOSTART, + Method.HF, + BasisSet.DEF2_SVP, + Task.SP, + ) + + # calc.input.add_arbitrary_string("!hf hf hf") + # calc.input.add_arbitrary_string("%novalidblock") + scf_block = BlockScf() + scf_block.add_option(name="maxiter", val="invalid_key") + calc.input.add_blocks(scf_block) + + # > write the input and run the calculation + calc.write_input() + calc.run() + + # > get the output and check some results + 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 + + # > Parse JSON files + output.parse() + + # check for convergence of the SCF + if output.results_properties.geometries[0].single_point_data.converged: + print("SCF CONVERGED") + else: + print("SCF DID NOT CONVERGE") + sys.exit(1) + + print("FINAL SINGLE POINT ENERGY") + print(output.get_final_energy()) + # > is equal to + print(output.results_properties.geometries[-1].single_point_data.finalenergy) + + return output + + +if __name__ == "__main__": + output = unknown_block_value() diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index 045d321b..9a009f75 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -32,10 +32,20 @@ class ErrorPattern: def _simple_keywords(grep_string: str, grepper: Grepper) -> str: """parse "Unknown keyword: BLYPP" from the matched line""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) - return f"Unknown/duplicate keyword(s): {match[0]}" if match else "" + return f"Unknown/duplicate simple keyword(s): {match[0]}" if match else "" def _unknown_block(grep_string: str, grepper: Grepper) -> str: """""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=0) - return f"Unknown Block: {match[0].split()[-1]}" if match else "" + return f"Unknown block: {match[0].split()[-1]}" if match else "" + +def _unknown_block_key(grep_string: str, grepper: Grepper) -> str: + """parse "Unknown keyword: BLYPP" from the matched line""" + match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) + return f"Unknown block key: {match[0].split(':')[-1]}" if match else "" + +def _unknown_block_value(grep_string: str, grepper: Grepper) -> str: + """""" + match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) + return f"Unknown block value: {match[0].split(':')[-1]}" if match else "" diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index 2a32a847..2bd5b8dc 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -1,4 +1,4 @@ -from opi.output.grepper.error_pattern import ErrorPattern, _simple_keywords, _unknown_block +from opi.output.grepper.error_pattern import ErrorPattern, _simple_keywords, _unknown_block, _unknown_block_key, _unknown_block_value # > Success strings TERMINATED_NORMALLY = "****ORCA TERMINATED NORMALLY****" @@ -22,11 +22,11 @@ "An unrecognized or duplicated simple keyword was requested", extractor=_simple_keywords, ), - ErrorPattern("Unknown identifier", "An unknown block was requested", extractor=_unknown_block), + ErrorPattern("Invalid assignment", "An invalid value was requested in a block", extractor=_unknown_block_value), ErrorPattern( - "Unknown identifier in", "An unknown block option was requested", extractor=_unknown_block + "Unknown identifier in", "An unknown block option was requested", extractor=_unknown_block_key ), - ErrorPattern("Invalid assignment", "An invalid value was requested in a block"), + ErrorPattern("Unknown identifier", "An unknown block was requested", extractor=_unknown_block), ErrorPattern( "The Coupled-Cluster iterations have NOT converged", "Coupled-Cluster did not converge" ), From cac8c6e4112dcc8b025ebebfab597638814843c6 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:46:29 +0200 Subject: [PATCH 06/14] Run nox --- .../failures/001_unknown_simple_keyword/job.py | 1 - examples/failures/002_unknown_block/job.py | 1 - src/opi/output/grepper/error_pattern.py | 2 ++ src/opi/output/grepper/patterns.py | 18 +++++++++++++++--- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/failures/001_unknown_simple_keyword/job.py b/examples/failures/001_unknown_simple_keyword/job.py index e324ee5c..801107c8 100755 --- a/examples/failures/001_unknown_simple_keyword/job.py +++ b/examples/failures/001_unknown_simple_keyword/job.py @@ -5,7 +5,6 @@ from pathlib import Path from opi.core import Calculator -from opi.input.blocks import BlockScf from opi.input.simple_keywords import BasisSet, Method, Scf, Task from opi.input.structures import Structure from opi.output.core import Output diff --git a/examples/failures/002_unknown_block/job.py b/examples/failures/002_unknown_block/job.py index ad3e3c75..8ae17d07 100755 --- a/examples/failures/002_unknown_block/job.py +++ b/examples/failures/002_unknown_block/job.py @@ -5,7 +5,6 @@ from pathlib import Path from opi.core import Calculator -from opi.input.blocks import BlockScf from opi.input.simple_keywords import BasisSet, Method, Scf, Task from opi.input.structures import Structure from opi.output.core import Output diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index 9a009f75..acfeeef9 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -40,11 +40,13 @@ def _unknown_block(grep_string: str, grepper: Grepper) -> str: match = grepper.search(grep_string, case_sensitive=True, skip_lines=0) return f"Unknown block: {match[0].split()[-1]}" if match else "" + def _unknown_block_key(grep_string: str, grepper: Grepper) -> str: """parse "Unknown keyword: BLYPP" from the matched line""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) return f"Unknown block key: {match[0].split(':')[-1]}" if match else "" + def _unknown_block_value(grep_string: str, grepper: Grepper) -> str: """""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index 2bd5b8dc..7fa12df3 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -1,4 +1,10 @@ -from opi.output.grepper.error_pattern import ErrorPattern, _simple_keywords, _unknown_block, _unknown_block_key, _unknown_block_value +from opi.output.grepper.error_pattern import ( + ErrorPattern, + _simple_keywords, + _unknown_block, + _unknown_block_key, + _unknown_block_value, +) # > Success strings TERMINATED_NORMALLY = "****ORCA TERMINATED NORMALLY****" @@ -22,9 +28,15 @@ "An unrecognized or duplicated simple keyword was requested", extractor=_simple_keywords, ), - ErrorPattern("Invalid assignment", "An invalid value was requested in a block", extractor=_unknown_block_value), ErrorPattern( - "Unknown identifier in", "An unknown block option was requested", extractor=_unknown_block_key + "Invalid assignment", + "An invalid value was requested in a block", + extractor=_unknown_block_value, + ), + ErrorPattern( + "Unknown identifier in", + "An unknown block option was requested", + extractor=_unknown_block_key, ), ErrorPattern("Unknown identifier", "An unknown block was requested", extractor=_unknown_block), ErrorPattern( From 82e7a9fa9275ecb74c40f568565d19ce296591ae Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:01:30 +0200 Subject: [PATCH 07/14] Add invalid line error --- examples/failures/005_invalid_line/job.py | 68 +++++++++++++++++++++++ src/opi/output/grepper/error_pattern.py | 18 ++++-- src/opi/output/grepper/patterns.py | 3 +- 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100755 examples/failures/005_invalid_line/job.py diff --git a/examples/failures/005_invalid_line/job.py b/examples/failures/005_invalid_line/job.py new file mode 100755 index 00000000..2626e7fc --- /dev/null +++ b/examples/failures/005_invalid_line/job.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +import shutil +import sys +from pathlib import Path + +from opi.core import Calculator +from opi.input.blocks import BlockScf +from opi.input.simple_keywords import BasisSet, Method, Scf, Task +from opi.input.structures import Structure +from opi.output.core import Output + + +def unknown_block_value( + structure: Structure | None = None, working_dir: Path | None = Path("RUN") +) -> Output: + # > recreate the working dir + shutil.rmtree(working_dir, ignore_errors=True) + working_dir.mkdir() + + # > if no structure is given take a smiles + if structure is None: + structure = Structure.from_smiles("O") + + # > set up the calculator + calc = Calculator(basename="job", working_dir=working_dir) + calc.structure = structure + calc.input.add_simple_keywords( + Scf.NOAUTOSTART, + Method.HF, + BasisSet.DEF2_SVP, + Task.SP, + ) + + calc.input.add_arbitrary_string("invalid_line some_more_invalid_stuff") + + # > write the input and run the calculation + calc.write_input() + calc.run() + + # > get the output and check some results + 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 + + # > Parse JSON files + output.parse() + + # check for convergence of the SCF + if output.results_properties.geometries[0].single_point_data.converged: + print("SCF CONVERGED") + else: + print("SCF DID NOT CONVERGE") + sys.exit(1) + + print("FINAL SINGLE POINT ENERGY") + print(output.get_final_energy()) + # > is equal to + print(output.results_properties.geometries[-1].single_point_data.finalenergy) + + return output + + +if __name__ == "__main__": + output = unknown_block_value() diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index acfeeef9..2fdf8521 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -1,6 +1,7 @@ # patterns.py from dataclasses import dataclass from typing import Callable +import re from opi.output.grepper.core import Grepper @@ -26,28 +27,35 @@ class ErrorPattern: extractor: Callable[[str, Grepper], str] | None = None -# > extractor functions for more elaborate error messages +# > Extractor functions for more elaborate error messages. +# > They extract additional information from the ORCA output file for more descriptive error messages. +def _invalid_line(grep_string: str, grepper: Grepper) -> str: + """Retrieves the first variable from an invalid line.""" + match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) + if match: + match = re.search(r'\((.+?)\)', match[0]) + match = match.group(1) + return f"Invalid line starting with: {match}" if match else "" def _simple_keywords(grep_string: str, grepper: Grepper) -> str: """parse "Unknown keyword: BLYPP" from the matched line""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) return f"Unknown/duplicate simple keyword(s): {match[0]}" if match else "" - def _unknown_block(grep_string: str, grepper: Grepper) -> str: - """""" + """Retrieves the name of an unknown block and returns it in a string.""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=0) return f"Unknown block: {match[0].split()[-1]}" if match else "" def _unknown_block_key(grep_string: str, grepper: Grepper) -> str: - """parse "Unknown keyword: BLYPP" from the matched line""" + """Retrieves the name of an unknown block key and returns it in a string.""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) return f"Unknown block key: {match[0].split(':')[-1]}" if match else "" def _unknown_block_value(grep_string: str, grepper: Grepper) -> str: - """""" + """Retrieves the name of an unknown block value and returns it in a string.""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) return f"Unknown block value: {match[0].split(':')[-1]}" if match else "" diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index 7fa12df3..8a0c5d4b 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -4,6 +4,7 @@ _unknown_block, _unknown_block_key, _unknown_block_value, + _invalid_line ) # > Success strings @@ -21,7 +22,7 @@ # > In decreasing order of priority ERROR_PATTERNS: list[ErrorPattern] = [ ErrorPattern( - "ERROR: expect a '$', '!', '%', '*' or '[' in the input", "Invalid input line in ORCA input" + "ERROR: expect a '$', '!', '%', '*' or '[' in the input", "Invalid input line in ORCA input",extractor=_invalid_line ), ErrorPattern( "UNRECOGNIZED OR DUPLICATED KEYWORD(S) IN SIMPLE INPUT LINE", From d047415b0de113f080c5516ad6e6e40dccd83c69 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:03:49 +0200 Subject: [PATCH 08/14] Fix mypy issues --- examples/failures/005_invalid_line/job.py | 1 - src/opi/output/grepper/error_pattern.py | 9 ++++++--- src/opi/output/grepper/patterns.py | 6 ++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/failures/005_invalid_line/job.py b/examples/failures/005_invalid_line/job.py index 2626e7fc..ab76d587 100755 --- a/examples/failures/005_invalid_line/job.py +++ b/examples/failures/005_invalid_line/job.py @@ -5,7 +5,6 @@ from pathlib import Path from opi.core import Calculator -from opi.input.blocks import BlockScf from opi.input.simple_keywords import BasisSet, Method, Scf, Task from opi.input.structures import Structure from opi.output.core import Output diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index 2fdf8521..cecd611a 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -1,7 +1,7 @@ # patterns.py +import re from dataclasses import dataclass from typing import Callable -import re from opi.output.grepper.core import Grepper @@ -30,19 +30,22 @@ class ErrorPattern: # > Extractor functions for more elaborate error messages. # > They extract additional information from the ORCA output file for more descriptive error messages. + def _invalid_line(grep_string: str, grepper: Grepper) -> str: """Retrieves the first variable from an invalid line.""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) if match: - match = re.search(r'\((.+?)\)', match[0]) - match = match.group(1) + m = re.search(r"\((.+?)\)", match[0]) + match = m.group(1) if m else None return f"Invalid line starting with: {match}" if match else "" + def _simple_keywords(grep_string: str, grepper: Grepper) -> str: """parse "Unknown keyword: BLYPP" from the matched line""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) return f"Unknown/duplicate simple keyword(s): {match[0]}" if match else "" + def _unknown_block(grep_string: str, grepper: Grepper) -> str: """Retrieves the name of an unknown block and returns it in a string.""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=0) diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index 8a0c5d4b..bc320540 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -1,10 +1,10 @@ from opi.output.grepper.error_pattern import ( ErrorPattern, + _invalid_line, _simple_keywords, _unknown_block, _unknown_block_key, _unknown_block_value, - _invalid_line ) # > Success strings @@ -22,7 +22,9 @@ # > In decreasing order of priority ERROR_PATTERNS: list[ErrorPattern] = [ ErrorPattern( - "ERROR: expect a '$', '!', '%', '*' or '[' in the input", "Invalid input line in ORCA input",extractor=_invalid_line + "ERROR: expect a '$', '!', '%', '*' or '[' in the input", + "Invalid input line in ORCA input", + extractor=_invalid_line, ), ErrorPattern( "UNRECOGNIZED OR DUPLICATED KEYWORD(S) IN SIMPLE INPUT LINE", From 04a65e2c9209e6780072d01095831afd983742e5 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:05:24 +0200 Subject: [PATCH 09/14] Small typo fix --- examples/failures/004_unknown_block_value/job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/failures/004_unknown_block_value/job.py b/examples/failures/004_unknown_block_value/job.py index 00fe3533..332b5713 100755 --- a/examples/failures/004_unknown_block_value/job.py +++ b/examples/failures/004_unknown_block_value/job.py @@ -35,7 +35,7 @@ def unknown_block_value( # calc.input.add_arbitrary_string("!hf hf hf") # calc.input.add_arbitrary_string("%novalidblock") scf_block = BlockScf() - scf_block.add_option(name="maxiter", val="invalid_key") + scf_block.add_option(name="maxiter", val="invalid_value") calc.input.add_blocks(scf_block) # > write the input and run the calculation From f2d435e2d871af902d4b8f29cff98af6a27c4e16 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:13:27 +0200 Subject: [PATCH 10/14] Move failure examples to tests - Since they are never intendet to be used as examples we move them to the tests folder --- src/opi/output/grepper/patterns.py | 4 ++ src/opi/output/grepper/recipes.py | 1 + .../failure_examples/test_invalid_line.py | 22 +++---- tests/failure_examples/test_no_coords.py | 57 +++++++++++++++++++ .../failure_examples/test_unknown_block.py | 22 +++---- .../test_unknown_block_key.py | 22 +++---- .../test_unknown_block_value.py | 24 +++----- .../test_unknown_simple_keyword.py | 24 ++++---- 8 files changed, 108 insertions(+), 68 deletions(-) rename examples/failures/005_invalid_line/job.py => tests/failure_examples/test_invalid_line.py (73%) create mode 100755 tests/failure_examples/test_no_coords.py rename examples/failures/002_unknown_block/job.py => tests/failure_examples/test_unknown_block.py (73%) rename examples/failures/003_unknown_block_key/job.py => tests/failure_examples/test_unknown_block_key.py (74%) rename examples/failures/004_unknown_block_value/job.py => tests/failure_examples/test_unknown_block_value.py (70%) rename examples/failures/001_unknown_simple_keyword/job.py => tests/failure_examples/test_unknown_simple_keyword.py (70%) diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index bc320540..4260d531 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -42,6 +42,10 @@ extractor=_unknown_block_key, ), ErrorPattern("Unknown identifier", "An unknown block was requested", extractor=_unknown_block), + ErrorPattern( + "You must have a [COORDS] ... [END] block in your input", + "No coordinates in the ORCA input.", + ), ErrorPattern( "The Coupled-Cluster iterations have NOT converged", "Coupled-Cluster did not converge" ), diff --git a/src/opi/output/grepper/recipes.py b/src/opi/output/grepper/recipes.py index 2f34c8ba..5d2fdc86 100644 --- a/src/opi/output/grepper/recipes.py +++ b/src/opi/output/grepper/recipes.py @@ -17,6 +17,7 @@ def get_error_messages(file_name: Path) -> list[str] | None: grepper = Grepper(file_name) hits: list[str] = [] for pattern in ERROR_PATTERNS: + # > put this into the pattern class, implement boolean if something was found match = grepper.search(pattern.grep_string, case_sensitive=True) if match: if pattern.extractor: diff --git a/examples/failures/005_invalid_line/job.py b/tests/failure_examples/test_invalid_line.py similarity index 73% rename from examples/failures/005_invalid_line/job.py rename to tests/failure_examples/test_invalid_line.py index ab76d587..4c8c7ac1 100755 --- a/examples/failures/005_invalid_line/job.py +++ b/tests/failure_examples/test_invalid_line.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -import shutil import sys -from pathlib import Path + +import pytest from opi.core import Calculator from opi.input.simple_keywords import BasisSet, Method, Scf, Task @@ -10,19 +10,15 @@ from opi.output.core import Output -def unknown_block_value( - structure: Structure | None = None, working_dir: Path | None = Path("RUN") -) -> Output: - # > recreate the working dir - shutil.rmtree(working_dir, ignore_errors=True) - working_dir.mkdir() +@pytest.mark.examples +@pytest.mark.orca +@pytest.mark.xfail +def test_invalid_line(tmp_path) -> Output: - # > if no structure is given take a smiles - if structure is None: - structure = Structure.from_smiles("O") + structure = Structure.from_smiles("O") # > set up the calculator - calc = Calculator(basename="job", working_dir=working_dir) + calc = Calculator(basename="job", working_dir=tmp_path) calc.structure = structure calc.input.add_simple_keywords( Scf.NOAUTOSTART, @@ -64,4 +60,4 @@ def unknown_block_value( if __name__ == "__main__": - output = unknown_block_value() + pytest.main([__file__, "-v", "-s"]) diff --git a/tests/failure_examples/test_no_coords.py b/tests/failure_examples/test_no_coords.py new file mode 100755 index 00000000..ccc8771a --- /dev/null +++ b/tests/failure_examples/test_no_coords.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import sys + +import pytest + +from opi.core import Calculator +from opi.input.simple_keywords import BasisSet, Method, Scf, Task +from opi.output.core import Output + + +@pytest.mark.examples +@pytest.mark.orca +@pytest.mark.xfail +def test_no_coords(tmp_path) -> Output: + + # > set up the calculator + calc = Calculator(basename="job", working_dir=tmp_path) + calc.input.add_simple_keywords( + Scf.NOAUTOSTART, + Method.HF, + BasisSet.DEF2_SVP, + Task.SP, + ) + + # > write the input and run the calculation + calc.write_input() + calc.run() + + # > get the output and check some results + 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 + + # > Parse JSON files + output.parse() + + # check for convergence of the SCF + if output.results_properties.geometries[0].single_point_data.converged: + print("SCF CONVERGED") + else: + print("SCF DID NOT CONVERGE") + sys.exit(1) + + print("FINAL SINGLE POINT ENERGY") + print(output.get_final_energy()) + # > is equal to + print(output.results_properties.geometries[-1].single_point_data.finalenergy) + + return output + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"]) diff --git a/examples/failures/002_unknown_block/job.py b/tests/failure_examples/test_unknown_block.py similarity index 73% rename from examples/failures/002_unknown_block/job.py rename to tests/failure_examples/test_unknown_block.py index 8ae17d07..d8ea8e3b 100755 --- a/examples/failures/002_unknown_block/job.py +++ b/tests/failure_examples/test_unknown_block.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -import shutil import sys -from pathlib import Path + +import pytest from opi.core import Calculator from opi.input.simple_keywords import BasisSet, Method, Scf, Task @@ -10,19 +10,15 @@ from opi.output.core import Output -def unknown_block( - structure: Structure | None = None, working_dir: Path | None = Path("RUN") -) -> Output: - # > recreate the working dir - shutil.rmtree(working_dir, ignore_errors=True) - working_dir.mkdir() +@pytest.mark.examples +@pytest.mark.orca +@pytest.mark.xfail +def test_unknown_block(tmp_path) -> Output: - # > if no structure is given take a smiles - if structure is None: - structure = Structure.from_smiles("O") + structure = Structure.from_smiles("O") # > set up the calculator - calc = Calculator(basename="job", working_dir=working_dir) + calc = Calculator(basename="job", working_dir=tmp_path) calc.structure = structure calc.input.add_simple_keywords( Scf.NOAUTOSTART, @@ -64,4 +60,4 @@ def unknown_block( if __name__ == "__main__": - output = unknown_block() + pytest.main([__file__, "-v", "-s"]) diff --git a/examples/failures/003_unknown_block_key/job.py b/tests/failure_examples/test_unknown_block_key.py similarity index 74% rename from examples/failures/003_unknown_block_key/job.py rename to tests/failure_examples/test_unknown_block_key.py index 7367a79b..38cba134 100755 --- a/examples/failures/003_unknown_block_key/job.py +++ b/tests/failure_examples/test_unknown_block_key.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -import shutil import sys -from pathlib import Path + +import pytest from opi.core import Calculator from opi.input.blocks import BlockScf @@ -11,19 +11,15 @@ from opi.output.core import Output -def unknown_block_key( - structure: Structure | None = None, working_dir: Path | None = Path("RUN") -) -> Output: - # > recreate the working dir - shutil.rmtree(working_dir, ignore_errors=True) - working_dir.mkdir() +@pytest.mark.examples +@pytest.mark.orca +@pytest.mark.xfail +def test_unknown_block_key(tmp_path) -> Output: - # > if no structure is given take a smiles - if structure is None: - structure = Structure.from_smiles("O") + structure = Structure.from_smiles("O") # > set up the calculator - calc = Calculator(basename="job", working_dir=working_dir) + calc = Calculator(basename="job", working_dir=tmp_path) calc.structure = structure calc.input.add_simple_keywords( Scf.NOAUTOSTART, @@ -67,4 +63,4 @@ def unknown_block_key( if __name__ == "__main__": - output = unknown_block_key() + pytest.main([__file__, "-v", "-s"]) diff --git a/examples/failures/004_unknown_block_value/job.py b/tests/failure_examples/test_unknown_block_value.py similarity index 70% rename from examples/failures/004_unknown_block_value/job.py rename to tests/failure_examples/test_unknown_block_value.py index 332b5713..429016df 100755 --- a/examples/failures/004_unknown_block_value/job.py +++ b/tests/failure_examples/test_unknown_block_value.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -import shutil import sys -from pathlib import Path + +import pytest from opi.core import Calculator from opi.input.blocks import BlockScf @@ -11,19 +11,15 @@ from opi.output.core import Output -def unknown_block_value( - structure: Structure | None = None, working_dir: Path | None = Path("RUN") -) -> Output: - # > recreate the working dir - shutil.rmtree(working_dir, ignore_errors=True) - working_dir.mkdir() +@pytest.mark.examples +@pytest.mark.orca +@pytest.mark.xfail +def test_unknown_block_value(tmp_path) -> Output: - # > if no structure is given take a smiles - if structure is None: - structure = Structure.from_smiles("O") + structure = Structure.from_smiles("O") # > set up the calculator - calc = Calculator(basename="job", working_dir=working_dir) + calc = Calculator(basename="job", working_dir=tmp_path) calc.structure = structure calc.input.add_simple_keywords( Scf.NOAUTOSTART, @@ -32,8 +28,6 @@ def unknown_block_value( Task.SP, ) - # calc.input.add_arbitrary_string("!hf hf hf") - # calc.input.add_arbitrary_string("%novalidblock") scf_block = BlockScf() scf_block.add_option(name="maxiter", val="invalid_value") calc.input.add_blocks(scf_block) @@ -69,4 +63,4 @@ def unknown_block_value( if __name__ == "__main__": - output = unknown_block_value() + pytest.main([__file__, "-v", "-s"]) diff --git a/examples/failures/001_unknown_simple_keyword/job.py b/tests/failure_examples/test_unknown_simple_keyword.py similarity index 70% rename from examples/failures/001_unknown_simple_keyword/job.py rename to tests/failure_examples/test_unknown_simple_keyword.py index 801107c8..5396e76c 100755 --- a/examples/failures/001_unknown_simple_keyword/job.py +++ b/tests/failure_examples/test_unknown_simple_keyword.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -import shutil import sys -from pathlib import Path + +import pytest from opi.core import Calculator from opi.input.simple_keywords import BasisSet, Method, Scf, Task @@ -10,19 +10,16 @@ from opi.output.core import Output -def unknown_simple_keyword( - structure: Structure | None = None, working_dir: Path | None = Path("RUN") -) -> Output: - # > recreate the working dir - shutil.rmtree(working_dir, ignore_errors=True) - working_dir.mkdir() +@pytest.mark.examples +@pytest.mark.orca +@pytest.mark.xfail +def test_unknown_simple_keyword(tmp_path) -> Output: - # > if no structure is given take a smiles - if structure is None: - structure = Structure.from_smiles("O") + # > load smiles + structure = Structure.from_smiles("O") # > set up the calculator - calc = Calculator(basename="job", working_dir=working_dir) + calc = Calculator(basename="job", working_dir=tmp_path) calc.structure = structure calc.input.add_simple_keywords( Scf.NOAUTOSTART, @@ -32,7 +29,6 @@ def unknown_simple_keyword( ) calc.input.add_arbitrary_string("!hf hf hf") - # calc.input.add_arbitrary_string("%novalidblock") # > write the input and run the calculation calc.write_input() @@ -65,4 +61,4 @@ def unknown_simple_keyword( if __name__ == "__main__": - output = unknown_simple_keyword() + pytest.main([__file__, "-v", "-s"]) From aba93cc5d8fcd4604c528462fa7c81cb50026ad6 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:02:41 +0200 Subject: [PATCH 11/14] Merge all failure tests into a single file --- src/opi/output/grepper/error_pattern.py | 2 +- tests/failure_examples/test_input_failure.py | 125 ++++++++++++++++++ tests/failure_examples/test_invalid_line.py | 63 --------- tests/failure_examples/test_no_coords.py | 57 -------- tests/failure_examples/test_unknown_block.py | 63 --------- .../test_unknown_block_key.py | 66 --------- .../test_unknown_block_value.py | 66 --------- .../test_unknown_simple_keyword.py | 64 --------- 8 files changed, 126 insertions(+), 380 deletions(-) create mode 100755 tests/failure_examples/test_input_failure.py delete mode 100755 tests/failure_examples/test_invalid_line.py delete mode 100755 tests/failure_examples/test_no_coords.py delete mode 100755 tests/failure_examples/test_unknown_block.py delete mode 100755 tests/failure_examples/test_unknown_block_key.py delete mode 100755 tests/failure_examples/test_unknown_block_value.py delete mode 100755 tests/failure_examples/test_unknown_simple_keyword.py diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index cecd611a..67337ffd 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -41,7 +41,7 @@ def _invalid_line(grep_string: str, grepper: Grepper) -> str: def _simple_keywords(grep_string: str, grepper: Grepper) -> str: - """parse "Unknown keyword: BLYPP" from the matched line""" + """Get the duplicate or unknown keywords from the ORCA output file.""" match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) return f"Unknown/duplicate simple keyword(s): {match[0]}" if match else "" diff --git a/tests/failure_examples/test_input_failure.py b/tests/failure_examples/test_input_failure.py new file mode 100755 index 00000000..ec640762 --- /dev/null +++ b/tests/failure_examples/test_input_failure.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +import pytest + +from opi.core import Calculator +from opi.input.blocks import BlockScf +from opi.input.structures import Structure + +""" +Contains ORCA examples of failures to test OPI´s error handling capabilities. +The functions error_message() will search in the ORCA output file for a respective error string and will compose the +error message we assert here. +""" + + +@pytest.fixture +def calc(tmp_path): + """Create a calculator object and return it.""" + calc = Calculator(basename="job", working_dir=tmp_path) + return calc + + +@pytest.fixture +def calc_water(calc): + """Create a calculator object with water as structure and return it.""" + calc.structure = Structure.from_smiles("O") + return calc + + +@pytest.mark.orca +def test_no_coords(calc): + """Test error_message for ORCA without coordinates""" + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + assert not output.terminated_normally() + assert output.error_message() == "No coordinates in the ORCA input." + + +@pytest.mark.orca +def test_invalid_line(calc_water): + """Test error_message for ORCA with an invalid line in the input.""" + # > Add invalid line to input + invalid_line = "invalid_line" + calc_water.input.add_arbitrary_string(invalid_line) + + # > write the input and run the calculation + calc_water.write_and_run() + + # > get the output and check some results + output = calc_water.get_output() + assert not output.terminated_normally() + assert output.error_message() == f"Invalid line starting with: {invalid_line.upper()}" + + +@pytest.mark.orca +def test_simple_keyword(calc_water): + """Test error_message for ORCA with duplicate simple keywords.""" + # > Add invalid line to input + simple_keyword = "! hf hf hf" + calc_water.input.add_arbitrary_string(simple_keyword) + + # > write the input and run the calculation + calc_water.write_and_run() + + # > get the output and check some results + output = calc_water.get_output() + assert not output.terminated_normally() + # > Since one HF is duplicate ORCA only prints two instead of three + assert output.error_message() == "Unknown/duplicate simple keyword(s): HF HF" + + +@pytest.mark.orca +def test_unknown_block(calc_water): + """Test error_message for ORCA with an unknown block.""" + # > Add invalid line to input + unknown_block = "%invalidblock" + calc_water.input.add_arbitrary_string(unknown_block) + + # > write the input and run the calculation + calc_water.write_and_run() + + # > get the output and check some results + output = calc_water.get_output() + assert not output.terminated_normally() + # > Remove the % in the beginning and make it upper case + assert output.error_message() == f"Unknown block: {unknown_block.upper()[1:]}" + + +@pytest.mark.orca +def test_unknown_block_key(calc_water): + """Test error_message for ORCA with an unknown block key.""" + # > Add invalid line to input + scf_block = BlockScf() + invalid_key = "invalid_key" + scf_block.add_option(name=invalid_key, val="none") + calc_water.input.add_blocks(scf_block) + + # > write the input and run the calculation + calc_water.write_and_run() + + # > get the output and check some results + output = calc_water.get_output() + assert not output.terminated_normally() + assert output.error_message() == f"Unknown block key: {invalid_key.upper()}" + + +@pytest.mark.orca +def test_unknown_block_value(calc_water): + """Test error_message for ORCA with an unknown block value.""" + + # > Add invalid line to input + scf_block = BlockScf() + invalid_value = "invalid_value" + scf_block.add_option(name="maxiter", val=invalid_value) + calc_water.input.add_blocks(scf_block) + + # > write the input and run the calculation + calc_water.write_and_run() + + # > get the output and check some results + output = calc_water.get_output() + assert not output.terminated_normally() + assert output.error_message() == f"Unknown block value: {invalid_value.upper()}" diff --git a/tests/failure_examples/test_invalid_line.py b/tests/failure_examples/test_invalid_line.py deleted file mode 100755 index 4c8c7ac1..00000000 --- a/tests/failure_examples/test_invalid_line.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -import pytest - -from opi.core import Calculator -from opi.input.simple_keywords import BasisSet, Method, Scf, Task -from opi.input.structures import Structure -from opi.output.core import Output - - -@pytest.mark.examples -@pytest.mark.orca -@pytest.mark.xfail -def test_invalid_line(tmp_path) -> Output: - - structure = Structure.from_smiles("O") - - # > set up the calculator - calc = Calculator(basename="job", working_dir=tmp_path) - calc.structure = structure - calc.input.add_simple_keywords( - Scf.NOAUTOSTART, - Method.HF, - BasisSet.DEF2_SVP, - Task.SP, - ) - - calc.input.add_arbitrary_string("invalid_line some_more_invalid_stuff") - - # > write the input and run the calculation - calc.write_input() - calc.run() - - # > get the output and check some results - 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 - - # > Parse JSON files - output.parse() - - # check for convergence of the SCF - if output.results_properties.geometries[0].single_point_data.converged: - print("SCF CONVERGED") - else: - print("SCF DID NOT CONVERGE") - sys.exit(1) - - print("FINAL SINGLE POINT ENERGY") - print(output.get_final_energy()) - # > is equal to - print(output.results_properties.geometries[-1].single_point_data.finalenergy) - - return output - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/failure_examples/test_no_coords.py b/tests/failure_examples/test_no_coords.py deleted file mode 100755 index ccc8771a..00000000 --- a/tests/failure_examples/test_no_coords.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -import pytest - -from opi.core import Calculator -from opi.input.simple_keywords import BasisSet, Method, Scf, Task -from opi.output.core import Output - - -@pytest.mark.examples -@pytest.mark.orca -@pytest.mark.xfail -def test_no_coords(tmp_path) -> Output: - - # > set up the calculator - calc = Calculator(basename="job", working_dir=tmp_path) - calc.input.add_simple_keywords( - Scf.NOAUTOSTART, - Method.HF, - BasisSet.DEF2_SVP, - Task.SP, - ) - - # > write the input and run the calculation - calc.write_input() - calc.run() - - # > get the output and check some results - 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 - - # > Parse JSON files - output.parse() - - # check for convergence of the SCF - if output.results_properties.geometries[0].single_point_data.converged: - print("SCF CONVERGED") - else: - print("SCF DID NOT CONVERGE") - sys.exit(1) - - print("FINAL SINGLE POINT ENERGY") - print(output.get_final_energy()) - # > is equal to - print(output.results_properties.geometries[-1].single_point_data.finalenergy) - - return output - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/failure_examples/test_unknown_block.py b/tests/failure_examples/test_unknown_block.py deleted file mode 100755 index d8ea8e3b..00000000 --- a/tests/failure_examples/test_unknown_block.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -import pytest - -from opi.core import Calculator -from opi.input.simple_keywords import BasisSet, Method, Scf, Task -from opi.input.structures import Structure -from opi.output.core import Output - - -@pytest.mark.examples -@pytest.mark.orca -@pytest.mark.xfail -def test_unknown_block(tmp_path) -> Output: - - structure = Structure.from_smiles("O") - - # > set up the calculator - calc = Calculator(basename="job", working_dir=tmp_path) - calc.structure = structure - calc.input.add_simple_keywords( - Scf.NOAUTOSTART, - Method.HF, - BasisSet.DEF2_SVP, - Task.SP, - ) - - calc.input.add_arbitrary_string("%novalidblock") - - # > write the input and run the calculation - calc.write_input() - calc.run() - - # > get the output and check some results - 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 - - # > Parse JSON files - output.parse() - - # check for convergence of the SCF - if output.results_properties.geometries[0].single_point_data.converged: - print("SCF CONVERGED") - else: - print("SCF DID NOT CONVERGE") - sys.exit(1) - - print("FINAL SINGLE POINT ENERGY") - print(output.get_final_energy()) - # > is equal to - print(output.results_properties.geometries[-1].single_point_data.finalenergy) - - return output - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/failure_examples/test_unknown_block_key.py b/tests/failure_examples/test_unknown_block_key.py deleted file mode 100755 index 38cba134..00000000 --- a/tests/failure_examples/test_unknown_block_key.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -import pytest - -from opi.core import Calculator -from opi.input.blocks import BlockScf -from opi.input.simple_keywords import BasisSet, Method, Scf, Task -from opi.input.structures import Structure -from opi.output.core import Output - - -@pytest.mark.examples -@pytest.mark.orca -@pytest.mark.xfail -def test_unknown_block_key(tmp_path) -> Output: - - structure = Structure.from_smiles("O") - - # > set up the calculator - calc = Calculator(basename="job", working_dir=tmp_path) - calc.structure = structure - calc.input.add_simple_keywords( - Scf.NOAUTOSTART, - Method.HF, - BasisSet.DEF2_SVP, - Task.SP, - ) - - scf_block = BlockScf() - scf_block.add_option(name="invalid_key", val="none") - calc.input.add_blocks(scf_block) - - # > write the input and run the calculation - calc.write_input() - calc.run() - - # > get the output and check some results - 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 - - # > Parse JSON files - output.parse() - - # check for convergence of the SCF - if output.results_properties.geometries[0].single_point_data.converged: - print("SCF CONVERGED") - else: - print("SCF DID NOT CONVERGE") - sys.exit(1) - - print("FINAL SINGLE POINT ENERGY") - print(output.get_final_energy()) - # > is equal to - print(output.results_properties.geometries[-1].single_point_data.finalenergy) - - return output - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/failure_examples/test_unknown_block_value.py b/tests/failure_examples/test_unknown_block_value.py deleted file mode 100755 index 429016df..00000000 --- a/tests/failure_examples/test_unknown_block_value.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -import pytest - -from opi.core import Calculator -from opi.input.blocks import BlockScf -from opi.input.simple_keywords import BasisSet, Method, Scf, Task -from opi.input.structures import Structure -from opi.output.core import Output - - -@pytest.mark.examples -@pytest.mark.orca -@pytest.mark.xfail -def test_unknown_block_value(tmp_path) -> Output: - - structure = Structure.from_smiles("O") - - # > set up the calculator - calc = Calculator(basename="job", working_dir=tmp_path) - calc.structure = structure - calc.input.add_simple_keywords( - Scf.NOAUTOSTART, - Method.HF, - BasisSet.DEF2_SVP, - Task.SP, - ) - - scf_block = BlockScf() - scf_block.add_option(name="maxiter", val="invalid_value") - calc.input.add_blocks(scf_block) - - # > write the input and run the calculation - calc.write_input() - calc.run() - - # > get the output and check some results - 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 - - # > Parse JSON files - output.parse() - - # check for convergence of the SCF - if output.results_properties.geometries[0].single_point_data.converged: - print("SCF CONVERGED") - else: - print("SCF DID NOT CONVERGE") - sys.exit(1) - - print("FINAL SINGLE POINT ENERGY") - print(output.get_final_energy()) - # > is equal to - print(output.results_properties.geometries[-1].single_point_data.finalenergy) - - return output - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/failure_examples/test_unknown_simple_keyword.py b/tests/failure_examples/test_unknown_simple_keyword.py deleted file mode 100755 index 5396e76c..00000000 --- a/tests/failure_examples/test_unknown_simple_keyword.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -import pytest - -from opi.core import Calculator -from opi.input.simple_keywords import BasisSet, Method, Scf, Task -from opi.input.structures import Structure -from opi.output.core import Output - - -@pytest.mark.examples -@pytest.mark.orca -@pytest.mark.xfail -def test_unknown_simple_keyword(tmp_path) -> Output: - - # > load smiles - structure = Structure.from_smiles("O") - - # > set up the calculator - calc = Calculator(basename="job", working_dir=tmp_path) - calc.structure = structure - calc.input.add_simple_keywords( - Scf.NOAUTOSTART, - Method.HF, - BasisSet.DEF2_SVP, - Task.SP, - ) - - calc.input.add_arbitrary_string("!hf hf hf") - - # > write the input and run the calculation - calc.write_input() - calc.run() - - # > get the output and check some results - 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 - - # > Parse JSON files - output.parse() - - # check for convergence of the SCF - if output.results_properties.geometries[0].single_point_data.converged: - print("SCF CONVERGED") - else: - print("SCF DID NOT CONVERGE") - sys.exit(1) - - print("FINAL SINGLE POINT ENERGY") - print(output.get_final_energy()) - # > is equal to - print(output.results_properties.geometries[-1].single_point_data.finalenergy) - - return output - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) From 24c8a3e6282a1fc73fc5e8f8cc9978c9e5f333bd Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:41:05 +0200 Subject: [PATCH 12/14] Refactor ErrorPattern class, Add error_messages for common convergence problems --- src/opi/input/blocks/block_method.py | 3 + src/opi/output/grepper/error_pattern.py | 130 +++++++++++++----- src/opi/output/grepper/patterns.py | 60 ++++---- src/opi/output/grepper/recipes.py | 17 +-- .../test_convergence_failure.py | 86 ++++++++++++ tests/failure_examples/test_input_failure.py | 2 +- 6 files changed, 224 insertions(+), 74 deletions(-) create mode 100755 tests/failure_examples/test_convergence_failure.py diff --git a/src/opi/input/blocks/block_method.py b/src/opi/input/blocks/block_method.py index ec73cbc0..87c067fe 100644 --- a/src/opi/input/blocks/block_method.py +++ b/src/opi/input/blocks/block_method.py @@ -127,6 +127,9 @@ class BlockMethod(Block): d3s8: float | None = None d3a2: float | None = None + # > Number of CPSCF iterations + z_maxiter: int | None = None + # > Options for Extopt ProgExt: InputFilePath | None = None # Path to wrapper script Ext_Params: str | None = None # Arbitrary optional command line arguments diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index 67337ffd..998fa508 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -1,15 +1,14 @@ # patterns.py import re -from dataclasses import dataclass from typing import Callable from opi.output.grepper.core import Grepper -@dataclass class ErrorPattern: """ Represents an error pattern in the ORCA output file. + More complex error patterns derive from this class and override the extractor Attributes ---------- @@ -17,48 +16,115 @@ class ErrorPattern: The string that is searched in the output file. message: str A human-readable error message of the given error pattern. - extractor: Callable | None, default = None - Optional function for extracting more details from the matched line + 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. """ - grep_string: str - message: str - extractor: Callable[[str, Grepper], str] | None = None + def __init__( + self, + grep_string: str | None = None, + message: str | None = None, + critical: bool = False, + extractor: Callable[[Grepper], str] | None = None, + ) -> None: + if grep_string is not None: + self.grep_string = grep_string + if message is not None: + self.message = message + if critical is not None: + self.critical = critical + self.extractor = extractor + + def match(self, grepper: Grepper) -> str | None: + hit = grepper.search(self.grep_string, case_sensitive=True) + 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: + 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 -# > Extractor functions for more elaborate error messages. -# > They extract additional information from the ORCA output file for more descriptive error messages. + 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 -def _invalid_line(grep_string: str, grepper: Grepper) -> str: - """Retrieves the first variable from an invalid line.""" - match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) - if match: - m = re.search(r"\((.+?)\)", match[0]) - match = m.group(1) if m else None - return f"Invalid line starting with: {match}" if match else "" +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 _simple_keywords(grep_string: str, grepper: Grepper) -> str: - """Get the duplicate or unknown keywords from the ORCA output file.""" - match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) - return f"Unknown/duplicate simple keyword(s): {match[0]}" if match else "" + 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 -def _unknown_block(grep_string: str, grepper: Grepper) -> str: - """Retrieves the name of an unknown block and returns it in a string.""" - match = grepper.search(grep_string, case_sensitive=True, skip_lines=0) - return f"Unknown block: {match[0].split()[-1]}" if match else "" +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 _unknown_block_key(grep_string: str, grepper: Grepper) -> str: - """Retrieves the name of an unknown block key and returns it in a string.""" - match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) - return f"Unknown block key: {match[0].split(':')[-1]}" if match else "" + 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 _unknown_block_value(grep_string: str, grepper: Grepper) -> str: - """Retrieves the name of an unknown block value and returns it in a string.""" - match = grepper.search(grep_string, case_sensitive=True, skip_lines=1) - return f"Unknown block value: {match[0].split(':')[-1]}" if match else "" + 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 diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index 4260d531..4ea91200 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -1,10 +1,10 @@ from opi.output.grepper.error_pattern import ( ErrorPattern, - _invalid_line, - _simple_keywords, - _unknown_block, - _unknown_block_key, - _unknown_block_value, + InvalidLineError, + SimpleKeywordsError, + UnknownBlockError, + UnknownBlockKeyError, + UnknownBlockValueError, ) # > Success strings @@ -18,40 +18,40 @@ HAS_SCF = "SCF SETTINGS" HAS_ABORTING = "aborting" -# > List of known error patterns -# > In decreasing order of priority +# > Error patterns in order of priority. +# > Critical errors are listed first and will stop scanning when matched. +# > Non-critical errors are listed after and will all be reported. ERROR_PATTERNS: list[ErrorPattern] = [ + # > Critical input errors - stop scanning on first match + InvalidLineError(), + SimpleKeywordsError(), + UnknownBlockValueError(), + UnknownBlockKeyError(), + UnknownBlockError(), ErrorPattern( - "ERROR: expect a '$', '!', '%', '*' or '[' in the input", - "Invalid input line in ORCA input", - extractor=_invalid_line, - ), - ErrorPattern( - "UNRECOGNIZED OR DUPLICATED KEYWORD(S) IN SIMPLE INPUT LINE", - "An unrecognized or duplicated simple keyword was requested", - extractor=_simple_keywords, - ), - ErrorPattern( - "Invalid assignment", - "An invalid value was requested in a block", - extractor=_unknown_block_value, + "You must have a [COORDS] ... [END] block in your input", + "No coordinates in the ORCA input.", + critical=True, ), + # > Non-critical convergence errors - all reported ErrorPattern( - "Unknown identifier in", - "An unknown block option was requested", - extractor=_unknown_block_key, + "Error (SHARK/CP-SCF Solver): Unfortunately, the calculation did not converge.", + "CP-SCF did not converge", + critical=True, ), - ErrorPattern("Unknown identifier", "An unknown block was requested", extractor=_unknown_block), ErrorPattern( - "You must have a [COORDS] ... [END] block in your input", - "No coordinates in the ORCA input.", + "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 Coupled-Cluster iterations have NOT converged", "Coupled-Cluster did not converge" + "The optimization did not converge", + "Geometry optimization did not converge", + critical=False, ), - ErrorPattern("CIS/TDA-DFT did not converge", "CIS/TDA-DFT did not converge"), - ErrorPattern("SCF NOT CONVERGED", "SCF did not converge"), - ErrorPattern("The optimization did not converge", "Geometry optimization did not converge"), + # > Unspecific errors - these could potentially be dropped ErrorPattern("ABORTING THE RUN", "ORCA aborted the run"), ErrorPattern("ERROR", "ORCA encountered an error"), ] diff --git a/src/opi/output/grepper/recipes.py b/src/opi/output/grepper/recipes.py index 5d2fdc86..117a2164 100644 --- a/src/opi/output/grepper/recipes.py +++ b/src/opi/output/grepper/recipes.py @@ -13,20 +13,15 @@ def get_error_messages(file_name: Path) -> list[str] | None: - """Return all matched error messages, or None if none found""" + """Return all errors from the output files.""" grepper = Grepper(file_name) hits: list[str] = [] for pattern in ERROR_PATTERNS: - # > put this into the pattern class, implement boolean if something was found - match = grepper.search(pattern.grep_string, case_sensitive=True) - if match: - if pattern.extractor: - # > If an extractor function is defined we get additional context and call the extractor - detail = pattern.extractor(pattern.grep_string, grepper) - else: - # > If no extractor function is defined we just print the designated message - detail = pattern.message - hits.append(detail) + msg = pattern.match(grepper) + if msg: + hits.append(msg) + if pattern.critical: + break return hits if hits else None diff --git a/tests/failure_examples/test_convergence_failure.py b/tests/failure_examples/test_convergence_failure.py new file mode 100755 index 00000000..d1a59bf6 --- /dev/null +++ b/tests/failure_examples/test_convergence_failure.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +import pytest + +from opi.core import Calculator +from opi.input.blocks import BlockGeom, BlockMdci, BlockMethod, BlockScf +from opi.input.simple_keywords import AuxBasisSet, Task, Wft +from opi.input.structures import Structure + +""" +Contains ORCA examples of convergence failures to test OPI´s error handling capabilities. +The functions error_message() will search in the ORCA output file for a respective error string and will compose the +error message we assert here. +""" + + +@pytest.fixture +def calc(tmp_path): + """Create a calculator object with a water structure and return it.""" + calc = Calculator(basename="job", working_dir=tmp_path) + calc.structure = Structure.from_smiles("O") + return calc + + +@pytest.mark.orca +def test_scf_fail(calc): + """Test error_message for SCF failure""" + calc.input.add_blocks(BlockScf(maxiter=1)) + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + assert not output.terminated_normally() + assert output.error_message() == "SCF did not converge" + + +def test_cc_fail(calc): + """Test error_message for CC not converging""" + calc.input.add_blocks(BlockMdci(maxiter=1)) + calc.input.add_simple_keywords(Wft.CCSD_T) + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + assert not output.terminated_normally() + assert output.error_message() == "Coupled-Cluster did not converge" + + +def test_dlpno_cc_fail(calc): + """Test error_message for DLPNO-CC not converging""" + calc.input.add_blocks(BlockMdci(maxiter=1)) + calc.input.add_simple_keywords(Wft.DLPNO_CCSD_T, AuxBasisSet.AUTOAUX) + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + assert not output.terminated_normally() + assert output.error_message() == "Coupled-Cluster did not converge" + + +def test_opt_fail(calc): + """Test error_message for optimization not converging""" + calc.input.add_blocks(BlockGeom(maxiter=1)) + calc.input.add_simple_keywords(Task.OPT) + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + assert output.terminated_normally() + assert output.error_message() == "Geometry optimization did not converge" + + +def test_cpscf_fail(calc): + """Test error_message for CP-SCF not converging""" + calc.input.add_blocks(BlockMethod(z_maxiter=1)) + calc.input.add_simple_keywords(Task.FREQ) + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + assert not output.terminated_normally() + assert output.error_message() == "CP-SCF did not converge" diff --git a/tests/failure_examples/test_input_failure.py b/tests/failure_examples/test_input_failure.py index ec640762..69735917 100755 --- a/tests/failure_examples/test_input_failure.py +++ b/tests/failure_examples/test_input_failure.py @@ -6,7 +6,7 @@ from opi.input.structures import Structure """ -Contains ORCA examples of failures to test OPI´s error handling capabilities. +Contains ORCA examples of input failures to test OPI´s error handling capabilities. The functions error_message() will search in the ORCA output file for a respective error string and will compose the error message we assert here. """ From 685e3175ccd9d4f24a1021df25cc4306c0ea9b07 Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:54:14 +0200 Subject: [PATCH 13/14] Memory failures initial --- src/opi/output/grepper/error_pattern.py | 22 ++++++ src/opi/output/grepper/patterns.py | 30 +++++++-- .../test_convergence_failure.py | 12 ++-- tests/failure_examples/test_memory_failure.py | 67 +++++++++++++++++++ 4 files changed, 121 insertions(+), 10 deletions(-) create mode 100755 tests/failure_examples/test_memory_failure.py diff --git a/src/opi/output/grepper/error_pattern.py b/src/opi/output/grepper/error_pattern.py index 998fa508..e70848f8 100644 --- a/src/opi/output/grepper/error_pattern.py +++ b/src/opi/output/grepper/error_pattern.py @@ -128,3 +128,25 @@ class UnknownBlockValueError(ErrorPattern): 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 diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index 4ea91200..ce6cb08d 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -1,6 +1,7 @@ from opi.output.grepper.error_pattern import ( ErrorPattern, InvalidLineError, + NotEnoughMemoryScfError, SimpleKeywordsError, UnknownBlockError, UnknownBlockKeyError, @@ -19,8 +20,8 @@ HAS_ABORTING = "aborting" # > Error patterns in order of priority. -# > Critical errors are listed first and will stop scanning when matched. -# > Non-critical errors are listed after and will all be reported. +# > Critical errors will stop scanning when matched. +# > Non-critical errors will just be added and reported. ERROR_PATTERNS: list[ErrorPattern] = [ # > Critical input errors - stop scanning on first match InvalidLineError(), @@ -33,7 +34,7 @@ "No coordinates in the ORCA input.", critical=True, ), - # > Non-critical convergence errors - all reported + # > Convergence errors ErrorPattern( "Error (SHARK/CP-SCF Solver): Unfortunately, the calculation did not converge.", "CP-SCF did not converge", @@ -51,7 +52,28 @@ "Geometry optimization did not converge", critical=False, ), - # > Unspecific errors - these could potentially be dropped + # > 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"), ] diff --git a/tests/failure_examples/test_convergence_failure.py b/tests/failure_examples/test_convergence_failure.py index d1a59bf6..68cdcd7e 100755 --- a/tests/failure_examples/test_convergence_failure.py +++ b/tests/failure_examples/test_convergence_failure.py @@ -22,7 +22,7 @@ def calc(tmp_path): @pytest.mark.orca -def test_scf_fail(calc): +def test_scf_conv_fail(calc): """Test error_message for SCF failure""" calc.input.add_blocks(BlockScf(maxiter=1)) # > write the input and run the calculation @@ -34,7 +34,7 @@ def test_scf_fail(calc): assert output.error_message() == "SCF did not converge" -def test_cc_fail(calc): +def test_cc_conv_fail(calc): """Test error_message for CC not converging""" calc.input.add_blocks(BlockMdci(maxiter=1)) calc.input.add_simple_keywords(Wft.CCSD_T) @@ -47,7 +47,7 @@ def test_cc_fail(calc): assert output.error_message() == "Coupled-Cluster did not converge" -def test_dlpno_cc_fail(calc): +def test_dlpno_cc_conv_fail(calc): """Test error_message for DLPNO-CC not converging""" calc.input.add_blocks(BlockMdci(maxiter=1)) calc.input.add_simple_keywords(Wft.DLPNO_CCSD_T, AuxBasisSet.AUTOAUX) @@ -60,8 +60,8 @@ def test_dlpno_cc_fail(calc): assert output.error_message() == "Coupled-Cluster did not converge" -def test_opt_fail(calc): - """Test error_message for optimization not converging""" +def test_opt_conv_fail(calc): + """Test error_message for geometry optimization not converging""" calc.input.add_blocks(BlockGeom(maxiter=1)) calc.input.add_simple_keywords(Task.OPT) # > write the input and run the calculation @@ -73,7 +73,7 @@ def test_opt_fail(calc): assert output.error_message() == "Geometry optimization did not converge" -def test_cpscf_fail(calc): +def test_cpscf_conv_fail(calc): """Test error_message for CP-SCF not converging""" calc.input.add_blocks(BlockMethod(z_maxiter=1)) calc.input.add_simple_keywords(Task.FREQ) diff --git a/tests/failure_examples/test_memory_failure.py b/tests/failure_examples/test_memory_failure.py new file mode 100755 index 00000000..4209fc61 --- /dev/null +++ b/tests/failure_examples/test_memory_failure.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import pytest + +from opi.core import Calculator +from opi.input.blocks import BlockMdci +from opi.input.simple_keywords import BasisSet, Scf, Wft +from opi.input.structures import Structure + +""" +Contains ORCA examples of memory failures to test OPI´s error handling capabilities. +The functions error_message() will search in the ORCA output file for a respective error string and will compose the +error message we assert here. +""" + + +@pytest.fixture +def calc(tmp_path): + """Create a calculator object with a water structure and return it.""" + calc = Calculator(basename="job", working_dir=tmp_path) + calc.structure = Structure.from_smiles("O") + return calc + + +@pytest.mark.orca +def test_scf_mem_fail(calc): + """Test error_message for SCF memory failure""" + calc.input.memory = 1 + calc.input.add_simple_keywords(BasisSet.DEF2_QZVPPD) + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + print(output.error_message()) + assert not output.terminated_normally() + assert ( + "Not enough memory available for SCF. Available: 1.0 MB" in output.error_message() + ) + + +@pytest.mark.orca +def test_mp2_mem_fail(calc): + """Test error_message for MP2 memory failure""" + calc.input.memory = 4 + calc.input.add_simple_keywords(Wft.MP2, BasisSet.DEF2_TZVP, Scf.NOITER) + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + assert not output.terminated_normally() + assert "Calculation ran out of memory" == output.error_message() + + +@pytest.mark.orca +def test_cc_mem_fail(calc): + """Test error_message for CC triples memory failure""" + calc.input.memory = 1 + calc.input.add_simple_keywords(Wft.CCSD_T, BasisSet.DEF2_SVP, Scf.NOITER) + calc.input.add_blocks(BlockMdci(maxcore=1)) + # > write the input and run the calculation + calc.write_and_run() + + # > get the output and check some results + output = calc.get_output() + assert not output.terminated_normally() + assert "Not enough memory for triples calculation" == output.error_message() From d1c9b7a94196af2a48b242c5000770fc1829b25a Mon Sep 17 00:00:00 2001 From: haneug <38649381+haneug@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:05:13 +0200 Subject: [PATCH 14/14] Add cc_converged and casscf_converged to output --- README.md | 2 +- examples/exmp002_scf_ccsdt/job.py | 7 +++++- examples/exmp014_led/job.py | 7 +++++- examples/exmp028_nevpt2/job.py | 4 +++- src/opi/output/core.py | 36 ++++++++++++++++++++++++++++++ src/opi/output/grepper/patterns.py | 1 + src/opi/output/grepper/recipes.py | 36 ++++++++++++++++++++++++++++++ 7 files changed, 89 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 71bcfcb0..17e415b4 100644 --- a/README.md +++ b/README.md @@ -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) 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. diff --git a/examples/exmp002_scf_ccsdt/job.py b/examples/exmp002_scf_ccsdt/job.py index cf7eaf8f..f0fee535 100755 --- a/examples/exmp002_scf_ccsdt/job.py +++ b/examples/exmp002_scf_ccsdt/job.py @@ -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() diff --git a/examples/exmp014_led/job.py b/examples/exmp014_led/job.py index 7a72b19a..4a370010 100644 --- a/examples/exmp014_led/job.py +++ b/examples/exmp014_led/job.py @@ -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() diff --git a/examples/exmp028_nevpt2/job.py b/examples/exmp028_nevpt2/job.py index bc94526c..b2b96519 100755 --- a/examples/exmp028_nevpt2/job.py +++ b/examples/exmp028_nevpt2/job.py @@ -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() diff --git a/src/opi/output/core.py b/src/opi/output/core.py index 4ecf2e3b..6a4c77f8 100644 --- a/src/opi/output/core.py +++ b/src/opi/output/core.py @@ -21,6 +21,8 @@ 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, @@ -697,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 + + 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. diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py index ce6cb08d..344f08cd 100644 --- a/src/opi/output/grepper/patterns.py +++ b/src/opi/output/grepper/patterns.py @@ -13,6 +13,7 @@ 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" diff --git a/src/opi/output/grepper/recipes.py b/src/opi/output/grepper/recipes.py index 117a2164..b64cb249 100644 --- a/src/opi/output/grepper/recipes.py +++ b/src/opi/output/grepper/recipes.py @@ -2,6 +2,8 @@ from opi.output.grepper.core import Grepper from opi.output.grepper.patterns import ( + CASSCF_CONVERGED, + CC_CONVERGED, ERROR_PATTERNS, GEOMETRY_CONVERGED, HAS_ABORTING, @@ -273,3 +275,37 @@ def has_scf_converged(file_name: Path, /) -> bool: True if expression is found in file else False """ return has_string_in_file(file_name, SCF_CONVERGED) + + +def has_casscf_converged(file_name: Path, /) -> bool: + """ + Searches for the message '---- THE CAS-SCF GRADIENT HAS CONVERGED ----' as indicator that the CAS-SCF converged. + + Parameter + --------- + file_name: Path + Name of the output file + + Returns + ------- + bool + True if expression is found in file else False + """ + return has_string_in_file(file_name, CASSCF_CONVERGED) + + +def has_cc_converged(file_name: Path, /) -> bool: + """ + Searches for the message 'The Coupled-Cluster iterations have converged' as indicator that the CC converged. + + Parameter + --------- + file_name: Path + Name of the output file + + Returns + ------- + bool + True if expression is found in file else False + """ + return has_string_in_file(file_name, CC_CONVERGED)