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/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/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/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/core.py b/src/opi/output/core.py index db542984..6a4c77f8 100644 --- a/src/opi/output/core.py +++ b/src/opi/output/core.py @@ -18,8 +18,11 @@ from opi.output.cube import CubeOutput from opi.output.gbw_suffix import GbwSuffix from opi.output.grepper.recipes import ( + get_error_message, get_float_from_line, get_lines_from_block, + has_casscf_converged, + has_cc_converged, has_geometry_optimization_converged, has_scf_converged, has_terminated_normally, @@ -672,6 +675,13 @@ def terminated_normally(self) -> bool: except FileNotFoundError: return False + def error_message(self) -> str | None: + outfile = self.get_outfile() + try: + return get_error_message(outfile) + except FileNotFoundError: + return "Output File Not Found" + def scf_converged(self) -> bool: """ Determine if ORCA SCF converged, by looking for "SUCCESS" in the ".out" file. @@ -689,6 +699,40 @@ def scf_converged(self) -> bool: except FileNotFoundError: return False + def casscf_converged(self) -> bool: + """ + Determine if ORCA CAS-SCF converged, by looking for "THE CAS-SCF GRADIENT HAS CONVERGED" in the ".out" file. + Check only if ORCA CAS-SCF was actually requested. + If the ".out" file does not exist, also return False. + + Returns + ------- + bool + True if string is found in ".out" file else False + """ + outfile = self.get_outfile() + try: + return has_casscf_converged(outfile) + except FileNotFoundError: + return False + + 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/error_pattern.py b/src/opi/output/grepper/error_pattern.py new file mode 100644 index 00000000..e70848f8 --- /dev/null +++ b/src/opi/output/grepper/error_pattern.py @@ -0,0 +1,152 @@ +# patterns.py +import re +from typing import Callable + +from opi.output.grepper.core import Grepper + + +class ErrorPattern: + """ + Represents an error pattern in the ORCA output file. + More complex error patterns derive from this class and override the extractor + + Attributes + ---------- + grep_string: str + The string that is searched in the output file. + message: str + A human-readable error message of the given error pattern. + critical: bool, default = False + When the error is critical we will stop searching for further errors after finding it. + extractor: Callable[[Grepper], str] | None, default = None + Optional function for extracting more details from the matched line. + + """ + + def __init__( + self, + grep_string: str | None = None, + 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 + + def extract(self, grepper: Grepper) -> str | None: + match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1) + return f"Unknown/duplicate simple keyword(s): {match[0]}" if match else None + + +class UnknownBlockError(ErrorPattern): + """ + Triggered when ORCA encounters an unknown block name in the input file, + i.e. a '%blockname' that ORCA does not recognize. + """ + + grep_string = "Unknown identifier" + message = "An unknown block was requested" + critical = True + + def extract(self, grepper: Grepper) -> str | None: + match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=0) + return f"Unknown block: {match[0].split()[-1]}" if match else None + + +class UnknownBlockKeyError(ErrorPattern): + """ + Triggered when ORCA encounters an unknown key inside a block, + i.e. a valid block name but an unrecognized option within it. + """ + + grep_string = "Unknown identifier in" + message = "An unknown block option was requested" + critical = True + + def extract(self, grepper: Grepper) -> str | None: + match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1) + return f"Unknown block key: {match[0].split(':')[-1]}" if match else None + + +class UnknownBlockValueError(ErrorPattern): + """ + Triggered when ORCA encounters an invalid value for a block option, + i.e. the key is recognized but the assigned value is not valid. + """ + + grep_string = "Invalid assignment" + message = "An invalid value was requested in a block" + critical = True + + def extract(self, grepper: Grepper) -> str | None: + match = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1) + return f"Unknown block value: {match[0].split(':')[-1]}" if match else None + + +class NotEnoughMemoryScfError(ErrorPattern): + """ + Triggered when there is not enough memory available for the SCF + """ + + grep_string = "Error (ORCA_SCF): Not enough memory available!" + message = "Not enough memory for SCF available" + critical = True + + def extract(self, grepper: Grepper) -> str | None: + mem_avail = grepper.search(self.grep_string, case_sensitive=True, skip_lines=1)[-1].split( + ":" + )[-1] + mem_estimated = grepper.search(self.grep_string, case_sensitive=True, skip_lines=2)[ + -1 + ].split(":")[-1] + if mem_estimated and mem_avail: + return f"Not enough memory available for SCF. Available: {mem_avail}, Required: {mem_estimated}" + else: + return None diff --git a/src/opi/output/grepper/patterns.py b/src/opi/output/grepper/patterns.py new file mode 100644 index 00000000..344f08cd --- /dev/null +++ b/src/opi/output/grepper/patterns.py @@ -0,0 +1,80 @@ +from opi.output.grepper.error_pattern import ( + ErrorPattern, + InvalidLineError, + NotEnoughMemoryScfError, + SimpleKeywordsError, + UnknownBlockError, + UnknownBlockKeyError, + UnknownBlockValueError, +) + +# > Success strings +TERMINATED_NORMALLY = "****ORCA TERMINATED NORMALLY****" +SCF_CONVERGED = "SUCCESS" +GEOMETRY_CONVERGED = "HURRAY" +CC_CONVERGED = "The Coupled-Cluster iterations have converged" +CASSCF_CONVERGED = "---- THE CAS-SCF GRADIENT HAS CONVERGED ----" + +# > Has strings +HAS_GEOMETRY_OPT = "Geometry Optimization Run" +HAS_SCF = "SCF SETTINGS" +HAS_ABORTING = "aborting" + +# > Error patterns in order of priority. +# > Critical errors will stop scanning when matched. +# > Non-critical errors will just be added and reported. +ERROR_PATTERNS: list[ErrorPattern] = [ + # > Critical input errors - stop scanning on first match + InvalidLineError(), + SimpleKeywordsError(), + UnknownBlockValueError(), + UnknownBlockKeyError(), + UnknownBlockError(), + ErrorPattern( + "You must have a [COORDS] ... [END] block in your input", + "No coordinates in the ORCA input.", + critical=True, + ), + # > Convergence errors + ErrorPattern( + "Error (SHARK/CP-SCF Solver): Unfortunately, the calculation did not converge.", + "CP-SCF did not converge", + critical=True, + ), + ErrorPattern( + "The Coupled-Cluster iterations have NOT converged", + "Coupled-Cluster did not converge", + critical=True, + ), + ErrorPattern("CIS/TDA-DFT did not converge", "CIS/TDA-DFT did not converge"), + ErrorPattern("SCF NOT CONVERGED", "SCF did not converge", critical=True), + ErrorPattern( + "The optimization did not converge", + "Geometry optimization did not converge", + critical=False, + ), + # > Memory Errors + NotEnoughMemoryScfError(), + ErrorPattern( + "Error (ORCA_MDCI): not enough memory for computing triples", + "Not enough memory for triples calculation", + critical=True, + ), + ErrorPattern("ERROR - OUT OF MEMORY !!!", "Calculation ran out of memory", critical=False), + # > Module terminates not normally + ErrorPattern( + "ORCA finished by error termination in MDCI", + "Error in MDCI part of the calculation", + critical=True, + ), + ErrorPattern( + "ORCA finished by error termination in MP2", + "Error in MP2 part of the calculation", + critical=True, + ), + # > Potentially MPI related error + ErrorPattern("-" * 74, "Potentially an Open MPI related error occurred.", critical=False), + # > Unspecific errors + ErrorPattern("ABORTING THE RUN", "ORCA aborted the run"), + ErrorPattern("ERROR", "ORCA encountered an error"), +] diff --git a/src/opi/output/grepper/recipes.py b/src/opi/output/grepper/recipes.py index acb8f10d..b64cb249 100644 --- a/src/opi/output/grepper/recipes.py +++ b/src/opi/output/grepper/recipes.py @@ -1,6 +1,36 @@ from pathlib import Path from opi.output.grepper.core import Grepper +from opi.output.grepper.patterns import ( + CASSCF_CONVERGED, + CC_CONVERGED, + 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 errors from the output files.""" + grepper = Grepper(file_name) + hits: list[str] = [] + for pattern in ERROR_PATTERNS: + msg = pattern.match(grepper) + if msg: + hits.append(msg) + if pattern.critical: + break + 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 +190,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 +206,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 +223,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 +240,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 +257,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 +274,38 @@ 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) + + +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) diff --git a/tests/failure_examples/test_convergence_failure.py b/tests/failure_examples/test_convergence_failure.py new file mode 100755 index 00000000..68cdcd7e --- /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_conv_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_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) + # > 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_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) + # > 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_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 + 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_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) + # > 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 new file mode 100755 index 00000000..69735917 --- /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 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. +""" + + +@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_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()