diff --git a/process/core/final.py b/process/core/final.py index be55db69cb..2d17e7f37f 100644 --- a/process/core/final.py +++ b/process/core/final.py @@ -8,6 +8,7 @@ from process.core.solver import constraints from process.core.solver.objectives import objective_function from process.data_structure import numerics +from process.data_structure.numerics import SolverOutputCondition def finalise(models, data, ifail: int, non_idempotent_msg: str | None = None): @@ -26,7 +27,7 @@ def finalise(models, data, ifail: int, non_idempotent_msg: str | None = None): non_idempotent_msg : None | str, optional warning about non-idempotent variables, defaults to None """ - if ifail == 1: + if ifail == SolverOutputCondition.CONVERGED: po.oheadr(constants.NOUT, "Final Feasible Point") else: po.oheadr(constants.NOUT, "Final UNFEASIBLE Point") diff --git a/process/core/io/vary_run/config.py b/process/core/io/vary_run/config.py index c267881d6a..5603f536ef 100644 --- a/process/core/io/vary_run/config.py +++ b/process/core/io/vary_run/config.py @@ -26,6 +26,7 @@ process_warnings, set_variable_in_indat, ) +from process.data_structure.numerics import SolverOutputCondition logger = logging.getLogger(__name__) @@ -156,7 +157,7 @@ def __next__(self): m_file = MFile(filename=self.wdir / mfile) ifail = m_file.data["ifail"].get_scan(-1) - if ifail != 1: + if ifail != SolverOutputCondition.CONVERGED: print(f"VaryRun iteration {self._current_iteration} did not converge.\n") else: print( @@ -243,7 +244,7 @@ def error_status2readme(self, mfile): error_status = "The MFILE is empty. PROCESS probably exited prematurely.\n" ifail = m_file.data["ifail"].get_scan(-1) - if ifail != 1: + if ifail != SolverOutputCondition.CONVERGED: ifail_msg = f"PROCESS has been unable to find a converging input file within the chosen maximum number of iterations.\nYou could try increasing the maximum number of iterations (which is currently set to {self.niter}),\nchanging the factor within which the iteration variables are changed,\nor by changing the initial values of the iteration variables." else: ifail_msg = f"PROCESS found a converged solution using VaryRun. The converging input file is {self._current_iteration - 1}_IN.DAT" diff --git a/process/core/io/vary_run/tools.py b/process/core/io/vary_run/tools.py index 641367f6c3..405662fd18 100644 --- a/process/core/io/vary_run/tools.py +++ b/process/core/io/vary_run/tools.py @@ -11,6 +11,7 @@ from process.core.io.in_dat import InDat from process.core.io.mfile import MFile from process.data_structure import numerics +from process.data_structure.numerics import SolverOutputCondition logger = logging.getLogger(__name__) @@ -250,13 +251,13 @@ def no_unfeasible_mfile(wdir=".", mfile="MFILE.DAT"): # no scans if not m_file.data["isweep"].exists: - if m_file.get("ifail") == 1: + if m_file.get("ifail") == SolverOutputCondition.CONVERGED: return 0 return 1 ifail = m_file.data["ifail"].get_scans() try: - return len(ifail) - ifail.count(1) + return len(ifail) - ifail.count(SolverOutputCondition.CONVERGED) except TypeError: # This seems to occur, if ifail is not in MFILE! # This probably means in the mfile library a KeyError @@ -317,7 +318,7 @@ def get_solution_from_mfile(neqns, nvars, wdir=".", mfile="MFILE.DAT"): table_sol = [m_file.get(f"itvar{var_no + 1:03}") for var_no in range(nvars)] table_res = [m_file.get(f"normres{con_no + 1:03}") for con_no in range(neqns)] - if ifail != 1: + if ifail != SolverOutputCondition.CONVERGED: return ifail, "0", "0", ["0"] * nvars, ["0"] * neqns return ifail, objective_function, constraints, table_sol, table_res diff --git a/process/core/scan.py b/process/core/scan.py index 5599ebf2f9..1f6f68bed1 100644 --- a/process/core/scan.py +++ b/process/core/scan.py @@ -22,6 +22,7 @@ scan_variables, tfcoil_variables, ) +from process.data_structure.numerics import SolverOutputCondition if TYPE_CHECKING: from process.core.model import DataStructure, Model @@ -272,7 +273,7 @@ def post_optimise(self, ifail: int): process_output.ocmmnt( constants.NOUT, "PROCESS has performed a VMCON (optimisation) run." ) - if ifail != 1: + if ifail != SolverOutputCondition.CONVERGED: process_output.ovarin(constants.NOUT, "Error flag", "(ifail)", ifail) process_output.oheadr( constants.IOTTY, "PROCESS COULD NOT FIND A FEASIBLE SOLUTION" @@ -397,7 +398,7 @@ def post_optimise(self, ifail: int): process_output.oblnkl(constants.NOUT) if self.solver == "fsolve": - if ifail == 1: + if ifail == SolverOutputCondition.CONVERGED: msg = "PROCESS has solved using fsolve." else: msg = "PROCESS failed to solve using fsolve." @@ -406,7 +407,7 @@ def post_optimise(self, ifail: int): f"{msg}\n", ) else: - if ifail == 1: + if ifail == SolverOutputCondition.CONVERGED: string1 = "PROCESS has successfully optimised" else: string1 = "PROCESS has failed to optimise" @@ -675,10 +676,10 @@ def verror(self, ifail: int): ifail: int : """ - if ifail == -1: + if ifail == SolverOutputCondition.USER_TERMINATED: process_output.ocmmnt(constants.NOUT, "User-terminated execution of VMCON.") process_output.ocmmnt(constants.IOTTY, "User-terminated execution of VMCON.") - elif ifail == 0: + elif ifail == SolverOutputCondition.IMPROPER_INPUT: process_output.ocmmnt( constants.NOUT, "Improper input parameters to the VMCON routine." ) @@ -688,7 +689,7 @@ def verror(self, ifail: int): constants.IOTTY, "Improper input parameters to the VMCON routine." ) process_output.ocmmnt(constants.IOTTY, "PROCESS coding must be checked.") - elif ifail == 2: + elif ifail == SolverOutputCondition.MAX_ITERATIONS: process_output.ocmmnt( constants.NOUT, "The maximum number of calls has been reached without solution.", @@ -729,7 +730,7 @@ def verror(self, ifail: int): constants.IOTTY, "Try changing the variables in IXC, or modify their initial values.", ) - elif ifail == 3: + elif ifail == SolverOutputCondition.MAX_LINE_SEARCHES: process_output.ocmmnt( constants.NOUT, "The line search required the maximum of 10 calls." ) @@ -749,7 +750,7 @@ def verror(self, ifail: int): process_output.ocmmnt( constants.IOTTY, "Try changing or adding variables to IXC." ) - elif ifail == 4: + elif ifail == SolverOutputCondition.UPHILL_SEARCH: process_output.ocmmnt( constants.NOUT, "An uphill search direction was found." ) @@ -765,7 +766,7 @@ def verror(self, ifail: int): constants.IOTTY, "Try changing the equations in ICC, or" ) process_output.ocmmnt(constants.IOTTY, "adding new variables to IXC.") - elif ifail == 5: + elif ifail == SolverOutputCondition.NO_SOLUTION: process_output.ocmmnt( constants.NOUT, "The quadratic programming technique was unable to" ) @@ -793,7 +794,7 @@ def verror(self, ifail: int): "their initial values (especially if only 1 optimisation", ) process_output.ocmmnt(constants.IOTTY, "iteration was performed).") - elif ifail == 6: + elif ifail == SolverOutputCondition.SINGULAR_MATRIX_OR_BOUNDS: process_output.ocmmnt( constants.NOUT, "The quadratic programming technique was restricted" ) diff --git a/process/core/solver/solver_handler.py b/process/core/solver/solver_handler.py index 5094c5a9e2..c708f45327 100644 --- a/process/core/solver/solver_handler.py +++ b/process/core/solver/solver_handler.py @@ -5,6 +5,7 @@ ) from process.core.solver.solver import get_solver from process.data_structure import numerics +from process.data_structure.numerics import SolverOutputCondition class SolverHandler: @@ -60,7 +61,7 @@ def run(self): # If VMCON optimisation has failed then try altering value of epsfcn if self.solver_name == "vmcon": - if ifail != 1: + if ifail != SolverOutputCondition.CONVERGED: print("Trying again with new epsfcn") # epsfcn is only used in evaluators.Evaluators() # TODO epsfcn could be set in Evaluators instance now, don't need to @@ -73,7 +74,7 @@ def run(self): # to next attempt numerics.epsfcn /= 10 # reset value - if ifail != 1: + if ifail != SolverOutputCondition.CONVERGED: print("Trying again with new epsfcn") numerics.epsfcn /= 10 # try new smaller value print("new epsfcn = ", numerics.epsfcn) @@ -83,7 +84,7 @@ def run(self): # If VMCON has exited with error code 5 try another run using a multiple # of the identity matrix as input for the Hessian b(n,n) # Only do this if VMCON has not iterated (nviter=1) - if ifail == 5 and numerics.nviter < 2: + if ifail == SolverOutputCondition.NO_SOLUTION and numerics.nviter < 2: print( "VMCON error code = 5. Rerunning VMCON with a new initial " "estimate of the second derivative matrix." diff --git a/process/data_structure/numerics.py b/process/data_structure/numerics.py index 937c9bd36a..01b84d7636 100644 --- a/process/data_structure/numerics.py +++ b/process/data_structure/numerics.py @@ -1,5 +1,37 @@ +from enum import IntEnum + import numpy as np + +class SolverOutputCondition(IntEnum): + """Enum for the possible conditions that can be returned by the solvers. + This is for the `ifail` condition + """ + + USER_TERMINATED = -1 + + IMPROPER_INPUT = 0 + """Solver failed due to improper input (e.g. invalid parameters, or failure to + satisfy solver preconditions)""" + CONVERGED = 1 + """Solver converged successfully""" + + MAX_ITERATIONS = 2 + """Solver failed to converge within the maximum number of iterations""" + + MAX_LINE_SEARCHES = 3 + """Line search required 10 function calls without finding a better solution""" + + UPHILL_SEARCH = 4 + """Uphill search direction was calculated""" + + NO_SOLUTION = 5 + """No feasible solution or bad approximation of Hessian""" + + SINGLE_MATRIX_OR_BOUNDS = 6 + """Singular matrix in quadratic subproblem or restriction by artificial bounds""" + + ipnvars: int = 177 """total number of variables available for iteration""" diff --git a/tests/integration/test_vmcon.py b/tests/integration/test_vmcon.py index 1630840081..f9257e3ff9 100644 --- a/tests/integration/test_vmcon.py +++ b/tests/integration/test_vmcon.py @@ -15,6 +15,7 @@ from process.core.init import init_all_module_vars from process.core.solver.evaluators import Evaluators from process.core.solver.solver import get_solver +from process.data_structure.numerics import SolverOutputCondition # Debug-level terminal output logging logger = logging.getLogger(__name__) @@ -75,7 +76,7 @@ def __init__(self): self.errlm = 0.0 self.errcom = 0.0 self.errcon = 0.0 - self.ifail = 1 + self.ifail = SolverOutputCondition.CONVERGED class CustomFunctionEvaluator(ABC, Evaluators): @@ -507,7 +508,7 @@ def get_case3(): case.exp.vlam = np.array([0.0, 0.0]) case.exp.errlg = 1.599997724349894 case.exp.errcon = 8.0000000000040417e-01 - case.exp.ifail = 5 + case.exp.ifail = SolverOutputCondition.NO_SOLUTION return case diff --git a/tests/regression/test_process_input_files.py b/tests/regression/test_process_input_files.py index 0b62666dd7..d30e9453a3 100644 --- a/tests/regression/test_process_input_files.py +++ b/tests/regression/test_process_input_files.py @@ -17,6 +17,7 @@ from regression_test_assets import RegressionTestAssetCollector from process.core.io.mfile import MFile +from process.data_structure.numerics import SolverOutputCondition from process.main import process_cli logger = logging.getLogger(__name__) @@ -116,7 +117,10 @@ def compare( ifail = mfile.data["ifail"].get_scan(-1) - assert ifail == 1 or mfile.data["ioptimz"].get_scan(-1) == -2, ( + assert ( + ifail == SolverOutputCondition.CONVERGED + or mfile.data["ioptimz"].get_scan(-1) == -2 + ), ( f"\033[0;36m ifail of {ifail} indicates PROCESS did not solve successfully\033[0m" ) diff --git a/tracking/tracking_data.py b/tracking/tracking_data.py index ba6d05c0bb..55eb118f6a 100644 --- a/tracking/tracking_data.py +++ b/tracking/tracking_data.py @@ -57,6 +57,7 @@ from bokeh.resources import CDN from process.core.io import mfile as mf +from process.data_structure.numerics import SolverOutputCondition logging.basicConfig(level=logging.INFO, filename="tracker.log") logger = logging.getLogger("PROCESS Tracker") @@ -182,7 +183,11 @@ def __init__( """ self.mfile = mf.MFile(mfile) - if strict and (ifail := self.mfile.data["ifail"].get_scan(-1)) != 1: + if ( + strict + and (ifail := self.mfile.data["ifail"].get_scan(-1)) + != SolverOutputCondition.CONVERGED + ): raise RuntimeError( f"{ifail = :.0f} indicates PROCESS has failed to converge." )