diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 65feab940..c61c08fde 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.14.0" +__version__ = "2.14.1" from .pyOpt_history import History from .pyOpt_variable import Variable diff --git a/pyoptsparse/pyALPSO/pyALPSO.py b/pyoptsparse/pyALPSO/pyALPSO.py index 670deb309..25af17618 100644 --- a/pyoptsparse/pyALPSO/pyALPSO.py +++ b/pyoptsparse/pyALPSO/pyALPSO.py @@ -190,8 +190,8 @@ def objconfunc(x): # Broadcast a -1 to indcate NSGA2 has finished self.optProb.comm.bcast(-1, root=0) - # Store Results - sol_inform = {"value": "", "text": ""} + # Optimizer has no exit conditions, so nothing to set + sol_inform = None # Create the optimization solution sol = self._createSolution(optTime, sol_inform, opt_f, opt_x) diff --git a/pyoptsparse/pyCONMIN/pyCONMIN.py b/pyoptsparse/pyCONMIN/pyCONMIN.py index 7539b1b63..f5c598001 100644 --- a/pyoptsparse/pyCONMIN/pyCONMIN.py +++ b/pyoptsparse/pyCONMIN/pyCONMIN.py @@ -239,8 +239,8 @@ def cnmngrad(n1, n2, x, f, g, ct, df, a, ic, nac): # Broadcast a -1 to indcate SLSQP has finished self.optProb.comm.bcast(-1, root=0) - # Store Results - sol_inform = {"value": "", "text": ""} + # Optimizer has no exit conditions, so nothing to set + sol_inform = None # Create the optimization solution sol = self._createSolution(optTime, sol_inform, ff, xs) diff --git a/pyoptsparse/pyIPOPT/pyIPOPT.py b/pyoptsparse/pyIPOPT/pyIPOPT.py index 4f0de8bea..1ef57239c 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -18,6 +18,7 @@ # Local modules from ..pyOpt_optimizer import Optimizer +from ..pyOpt_solution import SolutionInform from ..pyOpt_utils import ICOL, INFINITY, IROW, convertToCOO, extractRows, scaleRows @@ -274,10 +275,8 @@ def intermediate(_, *args, **kwargs): self.hist.writeData("metadata", self.metadata) self.hist.close() - # Store Results - sol_inform = {} - sol_inform["value"] = info["status"] - sol_inform["text"] = self.informs[info["status"]] + # Store optimizer exit condition and message + sol_inform = SolutionInform.from_informs(self.informs, info["status"]) # Create the optimization solution sol = self._createSolution(optTime, sol_inform, info["obj_val"], x, multipliers=info["mult_g"]) diff --git a/pyoptsparse/pyNLPQLP/pyNLPQLP.py b/pyoptsparse/pyNLPQLP/pyNLPQLP.py index 17436eed2..16efbb137 100644 --- a/pyoptsparse/pyNLPQLP/pyNLPQLP.py +++ b/pyoptsparse/pyNLPQLP/pyNLPQLP.py @@ -13,6 +13,7 @@ # Local modules from ..pyOpt_optimizer import Optimizer +from ..pyOpt_solution import SolutionInform from ..pyOpt_utils import try_import_compiled_module_from_path # import the compiled module @@ -254,11 +255,9 @@ def nlgrad(m, me, mmax, n, f, g, df, dg, x, active, wa): self.hist.writeData("metadata", self.metadata) self.hist.close() - # Store Results + # Store optimizer exit condition and message inform = ifail.item() - sol_inform = {} - sol_inform["value"] = inform - sol_inform["text"] = self.informs[inform] + sol_inform = SolutionInform.from_informs(self.informs, inform) # Create the optimization solution sol = self._createSolution(optTime, sol_inform, f, xs) diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index b77108280..d55994c82 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -189,8 +189,8 @@ def objconfunc(nreal, nobj, ncon, x, f, g): # Broadcast a -1 to indcate NSGA2 has finished self.optProb.comm.bcast(-1, root=0) - # Store Results - sol_inform = {"value": "", "text": ""} + # Optimizer has no exit conditions, so nothing to set + sol_inform = None xstar = [0.0] * n for i in range(n): diff --git a/pyoptsparse/pyOpt_history.py b/pyoptsparse/pyOpt_history.py index 9767cbfa2..b90d10805 100644 --- a/pyoptsparse/pyOpt_history.py +++ b/pyoptsparse/pyOpt_history.py @@ -221,7 +221,7 @@ def _processDB(self): if self.metadata["version"] != __version__: pyOptSparseWarning( - "The version of pyoptsparse used to generate the history file does not match the one being run right now. There may be compatibility issues." + f"The version of pyoptsparse used to generate the history file (v{self.metadata['version']}) does not match the one being run right now (v{__version__}). There may be compatibility issues." ) def getIterKeys(self): diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index f48224035..81ad0b15a 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -794,7 +794,7 @@ def _assembleObjective(self): return np.real(np.squeeze(ff)) - def _createSolution(self, optTime, sol_inform, obj, xopt, multipliers=None) -> Solution: + def _createSolution(self, optTime, solInform, obj, xopt, multipliers=None) -> Solution: """ Generic routine to create the solution after an optimizer finishes. @@ -824,7 +824,7 @@ def _createSolution(self, optTime, sol_inform, obj, xopt, multipliers=None) -> S "interfaceTime": self.interfaceTime - self.userSensTime - self.userObjTime, "optCodeTime": optTime - self.interfaceTime, } - sol = Solution(self.optProb, xStar, fStar, multipliers, sol_inform, info) + sol = Solution(self.optProb, xStar, fStar, multipliers, solInform, info) return sol diff --git a/pyoptsparse/pyOpt_solution.py b/pyoptsparse/pyOpt_solution.py index 9b26be66d..5de0fe05e 100644 --- a/pyoptsparse/pyOpt_solution.py +++ b/pyoptsparse/pyOpt_solution.py @@ -1,5 +1,7 @@ # Standard Python modules import copy +from dataclasses import dataclass +from typing import Optional # External modules import numpy as np @@ -8,8 +10,22 @@ from .pyOpt_optimization import Optimization +@dataclass(frozen=True) +class SolutionInform: + """Data class that contains the optimizer solution value and message""" + + value: int + """The integer return code""" + message: str + """The message string accompanying the return code""" + + @classmethod + def from_informs(cls, informs: dict[int, str], value: int): + return cls(value=value, message=informs[value]) + + class Solution(Optimization): - def __init__(self, optProb, xStar, fStar, lambdaStar, optInform, info): + def __init__(self, optProb, xStar, fStar, lambdaStar, optInform: Optional[SolutionInform], info): """ This class is used to describe the solution of an optimization problem. This class inherits from Optimization which enables a @@ -29,8 +45,9 @@ def __init__(self, optProb, xStar, fStar, lambdaStar, optInform, info): lambdaStar : dict The final Lagrange multipliers - optInform : int - The inform code returned by the optimizer + optInform : SolutionInform or None + Object containing the inform code and message returned by the optimizer. + Optimizers that do not have inform exit codes do not set this variable. info : dict A dictionary containing timing and call counter info to be stored @@ -95,12 +112,14 @@ def __str__(self) -> str: for i in range(5, len(lines)): text1 += lines[i] + "\n" - inform_val = self.optInform["value"] - inform_text = self.optInform["text"] - text1 += "\n" - text1 += " Exit Status\n" - text1 += " Inform Description\n" - text1 += f" {inform_val:>6} {inform_text:<0}\n" + # Only print exit status, inform, and description if the optimizer provides informs + if self.optInform: + inform_val = self.optInform.value + inform_text = self.optInform.message + text1 += "\n" + text1 += " Exit Status\n" + text1 += " Inform Description\n" + text1 += f" {inform_val:>6} {inform_text:<0}\n" text1 += ("-" * 80) + "\n" diff --git a/pyoptsparse/pyPSQP/pyPSQP.py b/pyoptsparse/pyPSQP/pyPSQP.py index 5eba85686..87bc156db 100644 --- a/pyoptsparse/pyPSQP/pyPSQP.py +++ b/pyoptsparse/pyPSQP/pyPSQP.py @@ -12,6 +12,7 @@ # Local modules from ..pyOpt_optimizer import Optimizer +from ..pyOpt_solution import SolutionInform from ..pyOpt_utils import try_import_compiled_module_from_path # import the compiled module @@ -259,9 +260,7 @@ def pdcon(n, k, x, dg): inform = iterm.item() if inform < 0 and inform not in self.informs: inform = -10 - sol_inform = {} - sol_inform["value"] = inform - sol_inform["text"] = self.informs[inform] + sol_inform = SolutionInform.from_informs(self.informs, inform) if self.storeHistory: self.metadata["endTime"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.metadata["optTime"] = optTime diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 2207239b2..15908b4a0 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -14,6 +14,7 @@ # Local modules from ..pyOpt_error import pyOptSparseWarning from ..pyOpt_optimizer import Optimizer +from ..pyOpt_solution import SolutionInform from ..pyOpt_utils import try_import_compiled_module_from_path # import the compiled module @@ -168,7 +169,7 @@ def __call__( # ================================================================= def slfunc(m, me, la, n, f, g, x): if (x < blx).any() or (x > bux).any(): - pyOptSparseWarning("Values in x were outside bounds during" " a minimize step, clipping to bounds") + pyOptSparseWarning("Values in x were outside bounds during a minimize step, clipping to bounds") fobj, fcon, fail = self._masterFunc(np.clip(x, blx, bux), ["fobj", "fcon"]) f = fobj g[0:m] = -fcon @@ -258,11 +259,9 @@ def slgrad(m, me, la, n, f, g, df, dg, x): # Broadcast a -1 to indcate SLSQP has finished self.optProb.comm.bcast(-1, root=0) - # Store Results + # Store optimizer exit condition and message inform = mode.item() - sol_inform = {} - sol_inform["value"] = inform - sol_inform["text"] = self.informs[inform] + sol_inform = SolutionInform.from_informs(self.informs, inform) # Create the optimization solution sol = self._createSolution(optTime, sol_inform, ff, xs) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index c6def1323..cfafd3ae5 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -20,6 +20,7 @@ # Local modules from ..pyOpt_optimization import Optimization from ..pyOpt_optimizer import Optimizer +from ..pyOpt_solution import SolutionInform from ..pyOpt_utils import ( ICOL, IDATA, @@ -499,10 +500,8 @@ def __call__( if iSumm != 0 and iSumm != 6: snopt.closeunit(self.getOption("iSumm")) - # Store Results - sol_inform = {} - sol_inform["value"] = inform - sol_inform["text"] = self.informs[inform] + # Store optimizer exit condition and message + sol_inform = SolutionInform.from_informs(self.informs, inform) # Create the optimization solution if parse_version(self.version) > parse_version("7.7.0") and parse_version(self.version) < parse_version( diff --git a/pyoptsparse/testing/pyOpt_testing.py b/pyoptsparse/testing/pyOpt_testing.py index e6dafabff..53aa55f9f 100644 --- a/pyoptsparse/testing/pyOpt_testing.py +++ b/pyoptsparse/testing/pyOpt_testing.py @@ -157,16 +157,16 @@ def assert_inform_equal(self, sol, optInform=None): ---------- sol : Solution object The solution object after optimization - optInform : int, optional + optInform : SolutionInform or None, optional The expected inform. If None, the default inform is used, which corresponds to a successful optimization termination. """ if optInform is not None: - self.assertEqual(sol.optInform["value"], optInform) + self.assertEqual(sol.optInform.value, optInform) else: # some optimizers do not have informs if self.optName in SUCCESS_INFORM: - self.assertEqual(sol.optInform["value"], SUCCESS_INFORM[self.optName]) + self.assertEqual(sol.optInform.value, SUCCESS_INFORM[self.optName]) def get_default_hst_name(self): # self.id() is provided by unittest.TestCase automatically diff --git a/tests/test_slsqp.py b/tests/test_slsqp.py index b7fa70e8e..2e459670a 100644 --- a/tests/test_slsqp.py +++ b/tests/test_slsqp.py @@ -41,6 +41,6 @@ def sens(xdict, funcs): optProb.addObj("obj") opt = OPT("SLSQP") sol = opt(optProb, sens=sens) - self.assertEqual(sol.optInform["value"], 0) + self.assertEqual(sol.optInform.value, 0) self.assertGreaterEqual(sol.xStar["xvars"][0], 0) self.assertAlmostEqual(sol.xStar["xvars"][0], 0, places=9) diff --git a/tests/test_user_termination.py b/tests/test_user_termination.py index 017255f34..b1fcde3a7 100644 --- a/tests/test_user_termination.py +++ b/tests/test_user_termination.py @@ -110,7 +110,7 @@ def test_obj(self, optName): self.assertEqual(termcomp.obj_count, 3) # Exit code for user requested termination. - self.assertEqual(sol.optInform["value"], self.optExitCode[optName]) + self.assertEqual(sol.optInform.value, self.optExitCode[optName]) @parameterized.expand(["IPOPT", "SNOPT"]) def test_sens(self, optName): @@ -131,7 +131,7 @@ def test_sens(self, optName): self.assertEqual(termcomp.sens_count, 4) # Exit code for user requested termination. - self.assertEqual(sol.optInform["value"], self.optExitCode[optName]) + self.assertEqual(sol.optInform.value, self.optExitCode[optName]) if __name__ == "__main__":