Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions process/core/final.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +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 PROCESSRunMode, SolverOutputCondition


def finalise(models, data, ifail: int, non_idempotent_msg: str | None = None):
Expand All @@ -26,7 +26,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")
Expand Down
5 changes: 3 additions & 2 deletions process/core/io/vary_run/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
set_variable_in_indat,
)
from process.core.model import DataStructure
from process.data_structure.numerics import SolverOutputCondition

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions process/core/io/vary_run/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
26 changes: 15 additions & 11 deletions process/core/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
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

if TYPE_CHECKING:
Expand Down Expand Up @@ -269,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"
Expand Down Expand Up @@ -409,7 +413,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."
Expand All @@ -418,7 +422,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"
Expand Down Expand Up @@ -702,10 +706,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."
)
Expand All @@ -715,7 +719,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.",
Expand Down Expand Up @@ -756,7 +760,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."
)
Expand All @@ -776,7 +780,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."
)
Expand All @@ -792,7 +796,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"
)
Expand Down Expand Up @@ -820,7 +824,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"
)
Expand Down
10 changes: 7 additions & 3 deletions process/core/solver/solver_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
load_scaled_bounds,
)
from process.core.solver.solver import get_solver
from process.data_structure.numerics import SolverOutputCondition


class SolverHandler:
Expand Down Expand Up @@ -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
Expand All @@ -72,7 +73,7 @@ def run(self):
# to next attempt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I agree with the copilot suggestion on this line, as all the checks now use SolverOutputCondition which means comments referencing ifail != 1 etc aren't as clear any more so should be updated where this occurs

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)
Expand All @@ -82,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 == 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."
Expand Down
29 changes: 29 additions & 0 deletions process/data_structure/numerics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@
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
"""
Comment on lines +8 to +11

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot comment is worth doing - some instances of if ifail == 1 remain


USER_TERMINATED = -1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

While self-explanatory, a short description here would be good for consistency


IMPROPER_INPUT = 0
"""Solver failed due to improper input (e.g. invalid parameters, or failure to
satisfy solver preconditions)"""
CONVERGED = 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
CONVERGED = 1
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"""
Comment on lines +24 to +28

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Might be worth noting that these are solver specific and won't always be raised (e.g. using fsolve or some custom solver)


NO_SOLUTION = 5
"""No feasible solution or bad approximation of Hessian"""

SINGLE_MATRIX_OR_BOUNDS = 6

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
SINGLE_MATRIX_OR_BOUNDS = 6
SINGLULAR_MATRIX_OR_BOUNDS = 6

"""Singular matrix in quadratic subproblem or restriction by artificial bounds"""
Comment on lines +15 to +34


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
Expand Down
5 changes: 3 additions & 2 deletions tests/integration/test_vmcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion tests/regression/test_process_input_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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"
)

Expand Down
7 changes: 6 additions & 1 deletion tracking/tracking_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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."
)
Expand Down
Loading