From 0c58c2e0948ba35cab9eb83f2bfcfec04deaee1f Mon Sep 17 00:00:00 2001 From: mn3981 Date: Wed, 27 May 2026 16:02:11 +0100 Subject: [PATCH 1/3] Add `ifail` enum to define solver output conditions --- process/data_structure/numerics.py | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/process/data_structure/numerics.py b/process/data_structure/numerics.py index 427467d30e..bc63c8af6a 100644 --- a/process/data_structure/numerics.py +++ b/process/data_structure/numerics.py @@ -2,9 +2,66 @@ from enum import IntEnum from types import DynamicClassAttribute +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 + """ + + 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""" + + + +class SolverOutputCondition(IntEnum): + """Enum for the possible conditions that can be returned by the solvers. + This is for the `ifail` condition + """ + + 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""" + + class PROCESSRunMode(IntEnum): """Enumeration of the available PROCESS run modes, which determine the behaviour of the code in various places. This is controlled by the `ioptimz` variable From 6396b581d0f69eb27c2a134a1b5bbd6fc6298e7f Mon Sep 17 00:00:00 2001 From: mn3981 Date: Wed, 27 May 2026 16:19:05 +0100 Subject: [PATCH 2/3] Refactor ifail checks to use SolverOutputCondition enum for improved clarity and maintainability --- process/core/final.py | 3 ++- process/core/io/vary_run/config.py | 5 +++-- process/core/io/vary_run/tools.py | 7 ++++--- process/core/scan.py | 21 ++++++++++---------- process/core/solver/solver_handler.py | 7 ++++--- process/data_structure/numerics.py | 2 ++ tests/integration/test_vmcon.py | 5 +++-- tests/regression/test_process_input_files.py | 6 +++++- tracking/tracking_data.py | 7 ++++++- 9 files changed, 40 insertions(+), 23 deletions(-) diff --git a/process/core/final.py b/process/core/final.py index d5da7593bc..8372e15951 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.numerics import PROCESSRunMode +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 4c6904d389..fa8808486f 100644 --- a/process/core/io/vary_run/config.py +++ b/process/core/io/vary_run/config.py @@ -25,6 +25,7 @@ process_warnings, set_variable_in_indat, ) +from process.data_structure.numerics import SolverOutputCondition from process.core.model import DataStructure logger = logging.getLogger(__name__) @@ -158,7 +159,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( @@ -245,7 +246,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 9d871213a7..0229396a52 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.core.model import DataStructure +from process.data_structure.numerics import SolverOutputCondition logger = logging.getLogger(__name__) @@ -254,13 +255,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 @@ -321,7 +322,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 b2da1471e0..7b59072271 100644 --- a/process/core/scan.py +++ b/process/core/scan.py @@ -17,6 +17,7 @@ from process.core.solver.solver_handler import SolverHandler from process.data_structure.numerics import FiguresOfMerit, PROCESSRunMode from process.data_structure.scan_variables import IPNSCNS, NOUTVARS, ScanData +from process.data_structure.numerics import SolverOutputCondition if TYPE_CHECKING: from process.core.model import DataStructure, Model @@ -269,7 +270,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" @@ -409,7 +410,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." @@ -418,7 +419,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" @@ -702,10 +703,10 @@ def verror(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." ) @@ -715,7 +716,7 @@ def verror(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.", @@ -756,7 +757,7 @@ def verror(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." ) @@ -776,7 +777,7 @@ def verror(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." ) @@ -792,7 +793,7 @@ def verror(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" ) @@ -820,7 +821,7 @@ def verror(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 b457e25ca9..517be00c50 100644 --- a/process/core/solver/solver_handler.py +++ b/process/core/solver/solver_handler.py @@ -4,6 +4,7 @@ load_scaled_bounds, ) from process.core.solver.solver import get_solver +from process.data_structure.numerics import SolverOutputCondition class SolverHandler: @@ -59,7 +60,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 @@ -72,7 +73,7 @@ def run(self): # to next attempt self.data.numerics.epsfcn /= 10 # reset value - if ifail != 1: + if ifail != SolverOutputCondition.CONVERGED: print("Trying again with new epsfcn") self.data.numerics.epsfcn /= 10 # try new smaller value print("new epsfcn = ", self.data.numerics.epsfcn) @@ -82,7 +83,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 self.data.numerics.nviter < 2: + if ifail == SolverOutputCondition.NO_SOLUTION and self.data.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 bc63c8af6a..4ce35294fe 100644 --- a/process/data_structure/numerics.py +++ b/process/data_structure/numerics.py @@ -12,6 +12,8 @@ class SolverOutputCondition(IntEnum): 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)""" diff --git a/tests/integration/test_vmcon.py b/tests/integration/test_vmcon.py index 112644e3e0..36200c8421 100644 --- a/tests/integration/test_vmcon.py +++ b/tests/integration/test_vmcon.py @@ -16,6 +16,7 @@ from process.core.model import DataStructure 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__) @@ -81,7 +82,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): @@ -523,7 +524,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 af3c751578..b2f6405fdf 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__) @@ -120,7 +121,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." ) From 0e7be6d39063b24ab4707187adef6c8f11089dcf Mon Sep 17 00:00:00 2001 From: mn3981 Date: Fri, 19 Jun 2026 12:01:31 +0100 Subject: [PATCH 3/3] Post rebase fixes --- process/core/final.py | 3 +-- process/core/io/vary_run/config.py | 2 +- process/core/scan.py | 7 +++++-- process/core/solver/solver_handler.py | 5 ++++- process/data_structure/numerics.py | 30 --------------------------- 5 files changed, 11 insertions(+), 36 deletions(-) diff --git a/process/core/final.py b/process/core/final.py index 8372e15951..55a42e8fc2 100644 --- a/process/core/final.py +++ b/process/core/final.py @@ -7,8 +7,7 @@ from process.core import process_output as po from process.core.solver import constraints from process.core.solver.objectives import objective_function -from process.data_structure.numerics import PROCESSRunMode -from process.data_structure.numerics import SolverOutputCondition +from process.data_structure.numerics import PROCESSRunMode, SolverOutputCondition def finalise(models, data, ifail: int, non_idempotent_msg: str | None = None): diff --git a/process/core/io/vary_run/config.py b/process/core/io/vary_run/config.py index fa8808486f..93b7c60548 100644 --- a/process/core/io/vary_run/config.py +++ b/process/core/io/vary_run/config.py @@ -25,8 +25,8 @@ process_warnings, set_variable_in_indat, ) -from process.data_structure.numerics import SolverOutputCondition from process.core.model import DataStructure +from process.data_structure.numerics import SolverOutputCondition logger = logging.getLogger(__name__) diff --git a/process/core/scan.py b/process/core/scan.py index 7b59072271..b39ca1aec2 100644 --- a/process/core/scan.py +++ b/process/core/scan.py @@ -15,9 +15,12 @@ from process.core.log import logging_model_handler, show_errors from process.core.solver import constraints from process.core.solver.solver_handler import SolverHandler -from process.data_structure.numerics import FiguresOfMerit, PROCESSRunMode +from process.data_structure.numerics import ( + FiguresOfMerit, + PROCESSRunMode, + SolverOutputCondition, +) from process.data_structure.scan_variables import IPNSCNS, NOUTVARS, ScanData -from process.data_structure.numerics import SolverOutputCondition if TYPE_CHECKING: from process.core.model import DataStructure, Model diff --git a/process/core/solver/solver_handler.py b/process/core/solver/solver_handler.py index 517be00c50..1d95388428 100644 --- a/process/core/solver/solver_handler.py +++ b/process/core/solver/solver_handler.py @@ -83,7 +83,10 @@ 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 == SolverOutputCondition.NO_SOLUTION and self.data.numerics.nviter < 2: + if ( + ifail == SolverOutputCondition.NO_SOLUTION + and self.data.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 4ce35294fe..813a2d083b 100644 --- a/process/data_structure/numerics.py +++ b/process/data_structure/numerics.py @@ -2,8 +2,6 @@ from enum import IntEnum from types import DynamicClassAttribute -from enum import IntEnum - import numpy as np @@ -36,34 +34,6 @@ class SolverOutputCondition(IntEnum): """Singular matrix in quadratic subproblem or restriction by artificial bounds""" - -class SolverOutputCondition(IntEnum): - """Enum for the possible conditions that can be returned by the solvers. - This is for the `ifail` condition - """ - - 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""" - - class PROCESSRunMode(IntEnum): """Enumeration of the available PROCESS run modes, which determine the behaviour of the code in various places. This is controlled by the `ioptimz` variable