From 55e022461e14d659a0700c74625119cc019b4a9a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 20 Jun 2024 07:05:13 -0600 Subject: [PATCH 01/25] Add FreeBSD to the set o known OSes --- pyomo/common/fileutils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/common/fileutils.py b/pyomo/common/fileutils.py index 7b6520327a0..8c3c6dfecaa 100644 --- a/pyomo/common/fileutils.py +++ b/pyomo/common/fileutils.py @@ -286,10 +286,17 @@ def find_dir( ) -_exeExt = {'linux': None, 'windows': '.exe', 'cygwin': '.exe', 'darwin': None} +_exeExt = { + 'linux': None, + 'freebsd': None, + 'windows': '.exe', + 'cygwin': '.exe', + 'darwin': None, +} _libExt = { 'linux': ('.so', '.so.*'), + 'freebsd': ('.so', '.so.*'), 'windows': ('.dll', '.pyd'), 'cygwin': ('.dll', '.so', '.so.*'), 'darwin': ('.dylib', '.so', '.so.*'), From 615c646b4510654a7d9d83ca762c15b5867a9fbd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 20 Jun 2024 07:05:39 -0600 Subject: [PATCH 02/25] Fix typo in test (exercised by unknown playform) --- pyomo/common/tests/test_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/tests/test_download.py b/pyomo/common/tests/test_download.py index 8fee0ba7e31..4fde029f1b1 100644 --- a/pyomo/common/tests/test_download.py +++ b/pyomo/common/tests/test_download.py @@ -206,7 +206,7 @@ def test_get_os_version(self): self.assertEqual(_os, 'win') self.assertEqual(_norm, _os + ''.join(_ver.split('.')[:2])) else: - self.assertEqual(ans, '') + self.assertEqual(_os, '') self.assertEqual((_os, _ver), FileDownloader._os_version) # Exercise the fetch from CACHE From e2941a433fff6043c8a2a6fa990939ee0d6df00b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 20 Jun 2024 07:06:34 -0600 Subject: [PATCH 03/25] Add `has_linear_solver()` to the IPOPT solver interfaces --- pyomo/contrib/appsi/solvers/ipopt.py | 20 ++++++++++++++++++++ pyomo/contrib/solver/ipopt.py | 15 +++++++++++++++ pyomo/solvers/plugins/solvers/IPOPT.py | 11 +++++++++++ 3 files changed, 46 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 76cd204e36d..97e8122fe78 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -567,3 +567,23 @@ def get_reduced_costs( return ComponentMap((k, v) for k, v in self._reduced_costs.items()) else: return ComponentMap((v, self._reduced_costs[v]) for v in vars_to_load) + + def has_linear_solver(self, linear_solver): + import pyomo.core as AML + from pyomo.common.tee import capture_output + m = AML.ConcreteModel() + m.x = AML.Var() + m.o = AML.Objective(expr=(m.x-2)**2) + with capture_output() as OUT: + solver = self.__class__() + solver.config.stream_solver = True + solver.config.load_solution = False + solver.ipopt_options['linear_solver'] = linear_solver + try: + solver.solve(m) + except FileNotFoundError: + # The APPSI interface always tries to open the SOL file, + # and will generate a FileNotFoundError if ipopt didn't + # generate one + return False + return 'running with linear solver' in OUT.getvalue() diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index c88696f531b..c467d283d9b 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -238,6 +238,21 @@ def version(self, config=None): self._version_cache = (pth, version) return self._version_cache[1] + def has_linear_solver(self, linear_solver): + import pyomo.core as AML + + m = AML.ConcreteModel() + m.x = AML.Var() + m.o = AML.Objective(expr=(m.x - 2) ** 2) + results = self.solve( + m, + tee=False, + raise_exception_on_nonoptimal_result=False, + load_solutions=False, + solver_options={'linear_solver': linear_solver}, + ) + return 'running with linear solver' in results.solver_log + def _write_options_file(self, filename: str, options: Mapping): # First we need to determine if we even need to create a file. # If options is empty, then we return False diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 21045cb7b4f..be0f143ea46 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -14,6 +14,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager from pyomo.opt.base import ProblemFormat, ResultsFormat @@ -207,3 +208,13 @@ def process_output(self, rc): res.solver.message = line.split(':')[2].strip() assert "degrees of freedom" in res.solver.message return res + + def has_linear_solver(self, linear_solver): + import pyomo.core as AML + + m = AML.ConcreteModel() + m.x = AML.Var() + m.o = AML.Objective(expr=(m.x - 2) ** 2) + with capture_output() as OUT: + self.solve(m, tee=True, options={'linear_solver': linear_solver}) + return 'running with linear solver' in OUT.getvalue() From f6fbfd47cad42dd6597d74621d1bcfa659208162 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 20 Jun 2024 07:19:51 -0600 Subject: [PATCH 04/25] Fix log message string formatting --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index a120add4200..e92aefee651 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -995,7 +995,7 @@ def run_grid_search( ) count += 1 failed_count += 1 - self.logger.warning("failed count:", failed_count) + self.logger.warning("failed count: %s", failed_count) result_combine[tuple(design_set_iter)] = None # For user's access From 800fe28475d3abae6a8983525c88b29bf6225747 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 20 Jun 2024 07:23:27 -0600 Subject: [PATCH 05/25] DoE: only specify linear solver if it is available. --- pyomo/contrib/doe/doe.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index e92aefee651..90818ddf622 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -97,7 +97,7 @@ def __init__( A Python ``function`` that returns a Concrete Pyomo model, similar to the interface for ``parmest`` solver: A ``solver`` object that User specified, default=None. - If not specified, default solver is IPOPT MA57. + If not specified, default solver is IPOPT (with MA57, if available). prior_FIM: A 2D numpy array containing Fisher information matrix (FIM) for prior experiments. The default None means there is no prior information. @@ -1387,7 +1387,10 @@ def _fix_design(self, m, design_val, fix_opt=True, optimize_option=None): def _get_default_ipopt_solver(self): """Default solver""" solver = SolverFactory("ipopt") - solver.options["linear_solver"] = "ma57" + for linear_solver in ('ma57', 'ma27', 'ma97'): + if solver.has_linear_solver(linear_solver): + solver.options["linear_solver"] = linear_solver + break solver.options["halt_on_ampl_error"] = "yes" solver.options["max_iter"] = 3000 return solver From 308d2a95bb3322ad7d52ff263c0c1b9625ec0b8f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 20 Jun 2024 07:26:35 -0600 Subject: [PATCH 06/25] ensure testing global sets are unregistered --- pyomo/core/tests/unit/test_set.py | 52 ++++++++++++++++++------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index f62589a6873..8b4c567b9ca 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -3518,21 +3518,25 @@ def test_iteration(self): def test_declare(self): NS = {} DeclareGlobalSet(RangeSet(name='TrinarySet', ranges=(NR(0, 2, 1),)), NS) - self.assertEqual(list(NS['TrinarySet']), [0, 1, 2]) - a = pickle.loads(pickle.dumps(NS['TrinarySet'])) - self.assertIs(a, NS['TrinarySet']) - with self.assertRaisesRegex(NameError, "name 'TrinarySet' is not defined"): - TrinarySet - del SetModule.GlobalSets['TrinarySet'] - del NS['TrinarySet'] + try: + self.assertEqual(list(NS['TrinarySet']), [0, 1, 2]) + a = pickle.loads(pickle.dumps(NS['TrinarySet'])) + self.assertIs(a, NS['TrinarySet']) + with self.assertRaisesRegex(NameError, "name 'TrinarySet' is not defined"): + TrinarySet + finally: + del SetModule.GlobalSets['TrinarySet'] + del NS['TrinarySet'] # Now test the automatic identification of the globals() scope DeclareGlobalSet(RangeSet(name='TrinarySet', ranges=(NR(0, 2, 1),))) - self.assertEqual(list(TrinarySet), [0, 1, 2]) - a = pickle.loads(pickle.dumps(TrinarySet)) - self.assertIs(a, TrinarySet) - del SetModule.GlobalSets['TrinarySet'] - del globals()['TrinarySet'] + try: + self.assertEqual(list(TrinarySet), [0, 1, 2]) + a = pickle.loads(pickle.dumps(TrinarySet)) + self.assertIs(a, TrinarySet) + finally: + del SetModule.GlobalSets['TrinarySet'] + del globals()['TrinarySet'] with self.assertRaisesRegex(NameError, "name 'TrinarySet' is not defined"): TrinarySet @@ -3551,18 +3555,22 @@ def test_exceptions(self): NS = {} ts = DeclareGlobalSet(RangeSet(name='TrinarySet', ranges=(NR(0, 2, 1),)), NS) - self.assertIs(NS['TrinarySet'], ts) + try: + self.assertIs(NS['TrinarySet'], ts) - # Repeat declaration is OK - DeclareGlobalSet(ts, NS) - self.assertIs(NS['TrinarySet'], ts) + # Repeat declaration is OK + DeclareGlobalSet(ts, NS) + self.assertIs(NS['TrinarySet'], ts) - # but conflicting one raises exception - NS['foo'] = None - with self.assertRaisesRegex( - RuntimeError, "Refusing to overwrite global object, foo" - ): - DeclareGlobalSet(RangeSet(name='foo', ranges=(NR(0, 2, 1),)), NS) + # but conflicting one raises exception + NS['foo'] = None + with self.assertRaisesRegex( + RuntimeError, "Refusing to overwrite global object, foo" + ): + DeclareGlobalSet(RangeSet(name='foo', ranges=(NR(0, 2, 1),)), NS) + finally: + del SetModule.GlobalSets['TrinarySet'] + del NS['TrinarySet'] def test_RealSet_IntegerSet(self): output = StringIO() From 3f843402f9a671d443871cc8b3f497e77ffff5d8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 20 Jun 2024 08:18:47 -0600 Subject: [PATCH 07/25] Fix state bleedover between tests --- pyomo/solvers/tests/mip/test_factory.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/tests/mip/test_factory.py b/pyomo/solvers/tests/mip/test_factory.py index 31d47486aa4..f69fd198009 100644 --- a/pyomo/solvers/tests/mip/test_factory.py +++ b/pyomo/solvers/tests/mip/test_factory.py @@ -53,8 +53,8 @@ def setUpClass(cls): def tearDown(self): ReaderFactory.unregister('rtest3') - ReaderFactory.unregister('stest3') - ReaderFactory.unregister('wtest3') + SolverFactory.unregister('stest3') + WriterFactory.unregister('wtest3') def test_solver_factory(self): """ @@ -119,6 +119,9 @@ def test_writer_instance(self): ans = WriterFactory("none") self.assertEqual(ans, None) ans = WriterFactory("wtest3") + self.assertEqual(ans, None) + WriterFactory.register('wtest3')(MockWriter) + ans = WriterFactory("wtest3") self.assertNotEqual(ans, None) def test_writer_registration(self): From c61d2d252300035c51a0eb05908337058379d787 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 20 Jun 2024 08:40:06 -0600 Subject: [PATCH 08/25] Guard tests that require k_aug --- pyomo/contrib/doe/tests/test_example.py | 2 ++ pyomo/contrib/doe/tests/test_reactor_example.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index e4ffbe89142..47ce39d596a 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -39,6 +39,7 @@ from pyomo.opt import SolverFactory ipopt_available = SolverFactory("ipopt").available() +k_aug_available = SolverFactory("k_aug").available(exception_flag=False) class TestReactorExamples(unittest.TestCase): @@ -57,6 +58,7 @@ def test_reactor_optimize_doe(self): reactor_optimize_doe.main() + @unittest.skipIf(not k_aug_available, "The 'k_aug' command is not available") @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index 19fb4e61820..f88ae48db1a 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -35,6 +35,7 @@ from pyomo.opt import SolverFactory ipopt_available = SolverFactory("ipopt").available() +k_aug_available = SolverFactory("k_aug").available(exception_flag=False) class Test_Reaction_Kinetics_Example(unittest.TestCase): @@ -133,6 +134,7 @@ def test_kinetics_example_sequential_finite_then_optimize(self): # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) self.assertAlmostEqual(np.log10(optimize_result.trace), 3.340, places=2) + @unittest.skipIf(not k_aug_available, "The 'k_aug' solver is not available") @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @unittest.skipIf(not pandas_available, "Pandas is not available") From 2455454a98ede2340c6f59545bc047c54c490170 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 21 Jun 2024 10:45:55 -0600 Subject: [PATCH 09/25] Update test guards to correctly skip when pynumero_ASL missing --- pyomo/contrib/parmest/tests/test_examples.py | 3 +- pyomo/contrib/parmest/tests/test_parmest.py | 74 ++++++++------------ 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index dca05026e80..450863a08a4 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -12,10 +12,11 @@ import pyomo.common.unittest as unittest import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.graphics import matplotlib_available, seaborn_available +from pyomo.contrib.pynumero.asl import AmplInterface from pyomo.opt import SolverFactory ipopt_available = SolverFactory("ipopt").available() - +pynumero_ASL_available = AmplInterface.available() @unittest.skipIf( not parmest.parmest_available, diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 65e2e4a3b06..1ff42a38d9e 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -9,44 +9,35 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, - matplotlib, - matplotlib_available, -) - import platform - -is_osx = platform.mac_ver()[0] != "" - -import pyomo.common.unittest as unittest import sys import os import subprocess from itertools import product +import pyomo.common.unittest as unittest import pyomo.contrib.parmest.parmest as parmest import pyomo.contrib.parmest.graphics as graphics import pyomo.contrib.parmest as parmestbase -from pyomo.contrib.parmest.experiment import Experiment import pyomo.environ as pyo import pyomo.dae as dae +from pyomo.common.dependencies import ( + numpy as np, + pandas as pd, + scipy, + matplotlib, +) +from pyomo.common.fileutils import this_file_dir +from pyomo.contrib.parmest.experiment import Experiment +from pyomo.contrib.pynumero.asl import AmplInterface from pyomo.opt import SolverFactory +is_osx = platform.mac_ver()[0] != "" ipopt_available = SolverFactory("ipopt").available() - -from pyomo.common.fileutils import find_library - -pynumero_ASL_available = False if find_library("pynumero_ASL") is None else True - -testdir = os.path.dirname(os.path.abspath(__file__)) - +k_aug_available = SolverFactory("k_aug").available(exception_flag=False) +pynumero_ASL_available = AmplInterface.available() +testdir = this_file_dir() @unittest.skipIf( not parmest.parmest_available, @@ -208,17 +199,13 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) self.assertEqual(retcode, 0) - @unittest.skip("Most folks don't have k_aug installed") + @unittest.skipUnless(k_aug_available, "k_aug solver not found") def test_theta_k_aug_for_Hessian(self): # this will fail if k_aug is not installed objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") self.assertAlmostEqual(objval, 4.4675, places=2) - @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") - @unittest.skipIf( - not parmest.inverse_reduced_hessian_available, - "Cannot test covariance matrix: required ASL dependency is missing", - ) + @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def test_theta_est_cov(self): objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) @@ -568,11 +555,7 @@ def SSE(model): }, } - @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") - @unittest.skipIf( - not parmest.inverse_reduced_hessian_available, - "Cannot test covariance matrix: required ASL dependency is missing", - ) + @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def check_rooney_biegler_results(self, objval, cov): # get indices in covariance matrix @@ -596,6 +579,7 @@ def check_rooney_biegler_results(self, objval, cov): cov.iloc[rate_constant_index, rate_constant_index], 0.04193591, places=2 ) # 0.04124 from paper + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): @@ -609,6 +593,7 @@ def test_parmest_basics(self): obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_initialize_parmest_model_option(self): for model_type, parmest_input in self.input.items(): @@ -625,6 +610,7 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_square_problem_solve(self): for model_type, parmest_input in self.input.items(): @@ -641,6 +627,7 @@ def test_parmest_basics_with_square_problem_solve(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): @@ -923,6 +910,7 @@ def test_return_continuous_set_multiple_datasets(self): self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_covariance(self): from pyomo.contrib.interior_point.inverse_reduced_hessian import ( inv_reduced_hessian_barrier, @@ -1217,17 +1205,13 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) assert retcode == 0 - @unittest.skip("Most folks don't have k_aug installed") + @unittest.skipUnless(k_aug_available, "k_aug solver not found") def test_theta_k_aug_for_Hessian(self): # this will fail if k_aug is not installed objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") self.assertAlmostEqual(objval, 4.4675, places=2) - @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") - @unittest.skipIf( - not parmest.inverse_reduced_hessian_available, - "Cannot test covariance matrix: required ASL dependency is missing", - ) + @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def test_theta_est_cov(self): objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) @@ -1485,11 +1469,7 @@ def SSE(model, data): }, } - @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") - @unittest.skipIf( - not parmest.inverse_reduced_hessian_available, - "Cannot test covariance matrix: required ASL dependency is missing", - ) + @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( @@ -1518,6 +1498,7 @@ def test_parmest_basics(self): obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_initialize_parmest_model_option(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( @@ -1549,6 +1530,7 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_square_problem_solve(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( @@ -1580,6 +1562,7 @@ def test_parmest_basics_with_square_problem_solve(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( @@ -1923,6 +1906,7 @@ def test_return_continuous_set_multiple_datasets(self): self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_covariance(self): from pyomo.contrib.interior_point.inverse_reduced_hessian import ( inv_reduced_hessian_barrier, From f4aeeedd902d0cd1c33108983ecced08a44d461b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 21 Jun 2024 10:46:11 -0600 Subject: [PATCH 10/25] Relax baseline comparisons --- pyomo/contrib/trustregion/tests/test_interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/trustregion/tests/test_interface.py b/pyomo/contrib/trustregion/tests/test_interface.py index 0922ccf950b..d241576f3ba 100644 --- a/pyomo/contrib/trustregion/tests/test_interface.py +++ b/pyomo/contrib/trustregion/tests/test_interface.py @@ -234,7 +234,7 @@ def test_updateSurrogateModel(self): for key, val in self.interface.data.grad_basis_model_output.items(): self.assertEqual(value(val), 0) for key, val in self.interface.data.truth_model_output.items(): - self.assertEqual(value(val), 0.8414709848078965) + self.assertAlmostEqual(value(val), 0.8414709848078965) # The truth gradients should equal the output of [cos(2-1), -cos(2-1)] truth_grads = [] for key, val in self.interface.data.grad_truth_model_output.items(): @@ -332,7 +332,7 @@ def test_calculateFeasibility(self): # Check after a solve is completed self.interface.data.basis_constraint.activate() objective, step_norm, feasibility = self.interface.solveModel() - self.assertEqual(feasibility, 0.09569982275514467) + self.assertAlmostEqual(feasibility, 0.09569982275514467) self.interface.data.basis_constraint.deactivate() @unittest.skipIf( @@ -361,7 +361,7 @@ def test_calculateStepSizeInfNorm(self): # Check after a solve is completed self.interface.data.basis_constraint.activate() objective, step_norm, feasibility = self.interface.solveModel() - self.assertEqual(step_norm, 3.393437471478297) + self.assertAlmostEqual(step_norm, 3.393437471478297) self.interface.data.basis_constraint.deactivate() @unittest.skipIf( From a1a75b855e151f2d03cef9b2343c260607d28c24 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 21 Jun 2024 10:46:48 -0600 Subject: [PATCH 11/25] Update compare_baseline to use both ABS an REL tolerance --- pyomo/common/unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 84d962eb784..f1fa01ac486 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -837,7 +837,7 @@ def filter_file_contents(self, lines, abstol=None): return filtered - def compare_baseline(self, test_output, baseline, abstol=1e-6, reltol=None): + def compare_baseline(self, test_output, baseline, abstol=1e-6, reltol=1e-8): # Filter files independently and then compare filtered contents out_filtered = self.filter_file_contents( test_output.strip().split('\n'), abstol From 73ca0fcab8bdac103b8445a7d102969e63030259 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:09:15 -0600 Subject: [PATCH 12/25] Guard additional tests for pynumero availability --- pyomo/contrib/parmest/tests/test_examples.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 450863a08a4..552c568cc7c 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -44,6 +44,7 @@ def test_model_with_constraint(self): rooney_biegler_with_constraint.main() + @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_parameter_estimation_example(self): from pyomo.contrib.parmest.examples.rooney_biegler import ( @@ -67,11 +68,12 @@ def test_likelihood_ratio_example(self): likelihood_ratio_example.main() -@unittest.skipIf( - not parmest.parmest_available, +@unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") +@unittest.skipUnless(ipopt_available, "The 'ipopt' solver is not available") +@unittest.skipUnless( + parmest.parmest_available, "Cannot test parmest: required dependencies are missing", ) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") class TestReactionKineticsExamples(unittest.TestCase): @classmethod def setUpClass(self): @@ -141,6 +143,7 @@ def test_model(self): reactor_design.main() + @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") def test_parameter_estimation_example(self): from pyomo.contrib.parmest.examples.reactor_design import ( parameter_estimation_example, From 31cb42014aeee6834da21945294165a3e529a612 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:09:53 -0600 Subject: [PATCH 13/25] Remove tests of k_aug functionality that is no longer supported --- pyomo/contrib/parmest/tests/test_parmest.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 1ff42a38d9e..fbe4290dd10 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -35,7 +35,6 @@ is_osx = platform.mac_ver()[0] != "" ipopt_available = SolverFactory("ipopt").available() -k_aug_available = SolverFactory("k_aug").available(exception_flag=False) pynumero_ASL_available = AmplInterface.available() testdir = this_file_dir() @@ -199,12 +198,6 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) self.assertEqual(retcode, 0) - @unittest.skipUnless(k_aug_available, "k_aug solver not found") - def test_theta_k_aug_for_Hessian(self): - # this will fail if k_aug is not installed - objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") - self.assertAlmostEqual(objval, 4.4675, places=2) - @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def test_theta_est_cov(self): objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) @@ -1205,12 +1198,6 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) assert retcode == 0 - @unittest.skipUnless(k_aug_available, "k_aug solver not found") - def test_theta_k_aug_for_Hessian(self): - # this will fail if k_aug is not installed - objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") - self.assertAlmostEqual(objval, 4.4675, places=2) - @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def test_theta_est_cov(self): objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) From 3ba93e555fbcb1ea3221cc028c0e1642bb488e3c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:11:17 -0600 Subject: [PATCH 14/25] Fix GJH version() to return the expected tuple --- pyomo/solvers/plugins/solvers/ASL.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index 7acd59936b1..c912f2a30ee 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -108,7 +108,7 @@ def _get_version(self): if ver is None: # Some ASL solvers do not export a version number if results.stdout.strip().split()[-1].startswith('ASL('): - return '0.0.0' + return (0, 0, 0) return ver except OSError: pass From d1c7952fa40cc340bb54bd87471e59a6d68a80ea Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:11:30 -0600 Subject: [PATCH 15/25] Update "pyomo help -s" - Silence all output generated when gathering solver availability / version information - Improve handling of version() not returning a tuple - Fix a bug where an exception raised when creating a solver caused an unhandled UnknownSolver error --- pyomo/scripting/driver_help.py | 64 ++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/pyomo/scripting/driver_help.py b/pyomo/scripting/driver_help.py index 38d1a4c16bf..ce7c36932ec 100644 --- a/pyomo/scripting/driver_help.py +++ b/pyomo/scripting/driver_help.py @@ -20,6 +20,7 @@ import pyomo.common from pyomo.common.collections import Bunch +from pyomo.common.tee import capture_output import pyomo.scripting.pyomo_parser logger = logging.getLogger('pyomo.solvers') @@ -235,33 +236,42 @@ def help_solvers(): try: # Disable warnings logging.disable(logging.WARNING) - for s in solver_list: - # Create a solver, and see if it is available - with pyomo.opt.SolverFactory(s) as opt: - ver = '' - if opt.available(False): - avail = '-' - if opt.license_is_valid(): - avail = '+' - try: - ver = opt.version() - if ver: - while len(ver) > 2 and ver[-1] == 0: - ver = ver[:-1] - ver = '.'.join(str(v) for v in ver) - else: - ver = '' - except (AttributeError, NameError): - pass - elif s == 'py' or (hasattr(opt, "_metasolver") and opt._metasolver): - # py is a metasolver, but since we don't specify a subsolver - # for this test, opt is actually an UnknownSolver, so we - # can't try to get the _metasolver attribute from it. - # Also, default to False if the attribute isn't implemented - avail = '*' - else: - avail = '' - _data.append((avail, s, ver, pyomo.opt.SolverFactory.doc(s))) + # suppress ALL output + with capture_output(capture_fd=True): + for s in solver_list: + # Create a solver, and see if it is available + with pyomo.opt.SolverFactory(s) as opt: + ver = '' + if opt.available(False): + avail = '-' + if opt.license_is_valid(): + avail = '+' + try: + ver = opt.version() + if isinstance(ver, str): + pass + elif ver: + while len(ver) > 2 and ver[-1] == 0: + ver = ver[:-1] + ver = '.'.join(str(v) for v in ver) + else: + ver = '' + except (AttributeError, NameError): + pass + elif isinstance(s, UnknownSolver): + # We can get here if creating a registered + # solver failed (i.e., an exception was raised + # in __init__) + avail = '' + elif s == 'py' or (hasattr(opt, "_metasolver") and opt._metasolver): + # py is a metasolver, but since we don't specify a subsolver + # for this test, opt is actually an UnknownSolver, so we + # can't try to get the _metasolver attribute from it. + # Also, default to False if the attribute isn't implemented + avail = '*' + else: + avail = '' + _data.append((avail, s, ver, pyomo.opt.SolverFactory.doc(s))) finally: # Reset logging level logging.disable(logging.NOTSET) From 8a4dcf1d1b32969681f877de3f1b3a8bb3d464d3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:17:01 -0600 Subject: [PATCH 16/25] Guard against platforms missing lsb_release --- pyomo/common/tests/test_download.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/common/tests/test_download.py b/pyomo/common/tests/test_download.py index 4fde029f1b1..4ee781d5738 100644 --- a/pyomo/common/tests/test_download.py +++ b/pyomo/common/tests/test_download.py @@ -22,7 +22,7 @@ import pyomo.common.envvar as envvar from pyomo.common import DeveloperError -from pyomo.common.fileutils import this_file +from pyomo.common.fileutils import this_file, Executable from pyomo.common.download import FileDownloader, distro_available from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output @@ -173,7 +173,8 @@ def test_get_os_version(self): self.assertTrue(v.replace('.', '').startswith(dist_ver)) if ( - subprocess.run( + Executable('lsb_release').available() + and subprocess.run( ['lsb_release'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, From 0a87a535ef1c07ca00077b904a1d631d7092cf00 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:18:59 -0600 Subject: [PATCH 17/25] Add missing import --- pyomo/repn/plugins/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index d3804c55106..ffe131b9b8b 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -37,6 +37,8 @@ def load(): def activate_writer_version(name, ver): """DEBUGGING TOOL to switch the "default" writer implementation""" + from pyomo.opt import WriterFactory + doc = WriterFactory.doc(name) WriterFactory.unregister(name) WriterFactory.register(name, doc)(WriterFactory.get_class(f'{name}_v{ver}')) From 353fb6c66cce5bfdb2bbded0801d5166f309ebb6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:19:41 -0600 Subject: [PATCH 18/25] NFC: apply black --- pyomo/contrib/appsi/solvers/ipopt.py | 3 ++- pyomo/contrib/parmest/tests/test_examples.py | 4 ++-- pyomo/contrib/parmest/tests/test_parmest.py | 8 ++------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 97e8122fe78..af40d2e88d2 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -571,9 +571,10 @@ def get_reduced_costs( def has_linear_solver(self, linear_solver): import pyomo.core as AML from pyomo.common.tee import capture_output + m = AML.ConcreteModel() m.x = AML.Var() - m.o = AML.Objective(expr=(m.x-2)**2) + m.o = AML.Objective(expr=(m.x - 2) ** 2) with capture_output() as OUT: solver = self.__class__() solver.config.stream_solver = True diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 552c568cc7c..3b0c869affa 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -18,6 +18,7 @@ ipopt_available = SolverFactory("ipopt").available() pynumero_ASL_available = AmplInterface.available() + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -71,8 +72,7 @@ def test_likelihood_ratio_example(self): @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") @unittest.skipUnless(ipopt_available, "The 'ipopt' solver is not available") @unittest.skipUnless( - parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", + parmest.parmest_available, "Cannot test parmest: required dependencies are missing" ) class TestReactionKineticsExamples(unittest.TestCase): @classmethod diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index fbe4290dd10..52b7cd390e8 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -22,12 +22,7 @@ import pyomo.environ as pyo import pyomo.dae as dae -from pyomo.common.dependencies import ( - numpy as np, - pandas as pd, - scipy, - matplotlib, -) +from pyomo.common.dependencies import numpy as np, pandas as pd, scipy, matplotlib from pyomo.common.fileutils import this_file_dir from pyomo.contrib.parmest.experiment import Experiment from pyomo.contrib.pynumero.asl import AmplInterface @@ -38,6 +33,7 @@ pynumero_ASL_available = AmplInterface.available() testdir = this_file_dir() + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", From 7ae66e33d29236caf37413106632d0f954fcfe05 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:36:25 -0600 Subject: [PATCH 19/25] fix UnknownSolver logic, missing import --- pyomo/scripting/driver_help.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyomo/scripting/driver_help.py b/pyomo/scripting/driver_help.py index ce7c36932ec..45fdc711137 100644 --- a/pyomo/scripting/driver_help.py +++ b/pyomo/scripting/driver_help.py @@ -258,16 +258,18 @@ def help_solvers(): ver = '' except (AttributeError, NameError): pass - elif isinstance(s, UnknownSolver): + elif s == 'py': + # py is a metasolver, but since we don't specify a subsolver + # for this test, opt is actually an UnknownSolver, so we + # can't try to get the _metasolver attribute from it. + avail = '*' + elif isinstance(s, pyomo.opt.solvers.UnknownSolver): # We can get here if creating a registered # solver failed (i.e., an exception was raised # in __init__) avail = '' - elif s == 'py' or (hasattr(opt, "_metasolver") and opt._metasolver): - # py is a metasolver, but since we don't specify a subsolver - # for this test, opt is actually an UnknownSolver, so we - # can't try to get the _metasolver attribute from it. - # Also, default to False if the attribute isn't implemented + elif getattr(opt, "_metasolver", False): + # Note: default to False if the attribute isn't implemented avail = '*' else: avail = '' From 0f79ae044118e3e7607bf09a89ea9bacd42bb05c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 22:27:46 -0600 Subject: [PATCH 20/25] Updating baseline due to new ipopt has_linear_solver method --- pyomo/contrib/solver/tests/unit/test_ipopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index cc459245506..9769eadecae 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -84,6 +84,7 @@ def test_class_member_list(self): 'CONFIG', 'config', 'available', + 'has_linear_solver', 'is_persistent', 'solve', 'version', From 2629cfdcdef022e11fa9458db883865e065ff38c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 23:41:22 -0600 Subject: [PATCH 21/25] Additional test guard for ipopt availability --- pyomo/contrib/doe/tests/test_fim_doe.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index d9a8d60fdb4..8891bd072a4 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -35,6 +35,9 @@ VariablesWithIndices, ) from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure +from pyomo.environ import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() class TestMeasurementError(unittest.TestCase): @@ -196,6 +199,7 @@ def test(self): @unittest.skipIf(not numpy_available, "Numpy is not available") +@unittest.skipIf(not ipopt_available, "Numpy is not available") class TestPriorFIMError(unittest.TestCase): def test(self): # Control time set [h] From a4946df48905ce94f3d8fc840374a03d30f5a599 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:53:45 -0600 Subject: [PATCH 22/25] Update pyomo/contrib/doe/tests/test_fim_doe.py --- pyomo/contrib/doe/tests/test_fim_doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index 8891bd072a4..9cae2fe6278 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -199,7 +199,7 @@ def test(self): @unittest.skipIf(not numpy_available, "Numpy is not available") -@unittest.skipIf(not ipopt_available, "Numpy is not available") +@unittest.skipIf(not ipopt_available, "ipopt is not available") class TestPriorFIMError(unittest.TestCase): def test(self): # Control time set [h] From 0855619fb68261e87ab3f03791032fa9b610e931 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 12:59:05 -0600 Subject: [PATCH 23/25] Add testing of ipopt.has_linear_solver() --- pyomo/contrib/appsi/tests/test_ipopt.py | 42 +++++++++++++++++++ pyomo/contrib/solver/tests/unit/test_ipopt.py | 23 ++++++++++ pyomo/solvers/plugins/solvers/IPOPT.py | 8 +++- pyomo/solvers/tests/checks/test_ipopt.py | 42 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 pyomo/contrib/appsi/tests/test_ipopt.py create mode 100644 pyomo/solvers/tests/checks/test_ipopt.py diff --git a/pyomo/contrib/appsi/tests/test_ipopt.py b/pyomo/contrib/appsi/tests/test_ipopt.py new file mode 100644 index 00000000000..b3697b9b233 --- /dev/null +++ b/pyomo/contrib/appsi/tests/test_ipopt.py @@ -0,0 +1,42 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.contrib.appsi.solvers import ipopt + + +ipopt_available = ipopt.Ipopt().available() + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptInterface(unittest.TestCase): + def test_has_linear_solver(self): + opt = ipopt.Ipopt() + self.assertTrue( + any( + map( + opt.has_linear_solver, + [ + 'mumps', + 'ma27', + 'ma57', + 'ma77', + 'ma86', + 'ma97', + 'pardiso', + 'pardisomkl', + 'spral', + 'wsmp', + ], + ) + ) + ) + self.assertFalse(opt.has_linear_solver('bogus_linear_solver')) diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index 9769eadecae..27a80feede0 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -168,6 +168,29 @@ def test_write_options_file(self): data = f.readlines() self.assertEqual(len(data), len(list(opt.config.solver_options.keys()))) + def test_has_linear_solver(self): + opt = ipopt.Ipopt() + self.assertTrue( + any( + map( + opt.has_linear_solver, + [ + 'mumps', + 'ma27', + 'ma57', + 'ma77', + 'ma86', + 'ma97', + 'pardiso', + 'pardisomkl', + 'spral', + 'wsmp', + ], + ) + ) + ) + self.assertFalse(opt.has_linear_solver('bogus_linear_solver')) + def test_create_command_line(self): opt = ipopt.Ipopt() # No custom options, no file created. Plain and simple. diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index be0f143ea46..82dcfdb75a0 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -14,6 +14,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.errors import ApplicationError from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager @@ -215,6 +216,9 @@ def has_linear_solver(self, linear_solver): m = AML.ConcreteModel() m.x = AML.Var() m.o = AML.Objective(expr=(m.x - 2) ** 2) - with capture_output() as OUT: - self.solve(m, tee=True, options={'linear_solver': linear_solver}) + try: + with capture_output() as OUT: + self.solve(m, tee=True, options={'linear_solver': linear_solver}) + except ApplicationError: + return False return 'running with linear solver' in OUT.getvalue() diff --git a/pyomo/solvers/tests/checks/test_ipopt.py b/pyomo/solvers/tests/checks/test_ipopt.py new file mode 100644 index 00000000000..b7d00c35a6f --- /dev/null +++ b/pyomo/solvers/tests/checks/test_ipopt.py @@ -0,0 +1,42 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.solvers.plugins.solvers import IPOPT +import pyomo.environ + +ipopt_available = IPOPT.IPOPT().available() + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptInterface(unittest.TestCase): + def test_has_linear_solver(self): + opt = IPOPT.IPOPT() + self.assertTrue( + any( + map( + opt.has_linear_solver, + [ + 'mumps', + 'ma27', + 'ma57', + 'ma77', + 'ma86', + 'ma97', + 'pardiso', + 'pardisomkl', + 'spral', + 'wsmp', + ], + ) + ) + ) + self.assertFalse(opt.has_linear_solver('bogus_linear_solver')) From cbd702dadd80e218a8e8597e511c57b2d0b449a9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 13:14:13 -0600 Subject: [PATCH 24/25] Add active_writer_version, test writer activation functions --- pyomo/repn/plugins/__init__.py | 15 ++++++++++ pyomo/repn/tests/test_plugins.py | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 pyomo/repn/tests/test_plugins.py diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index ffe131b9b8b..4029f44a03d 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -42,3 +42,18 @@ def activate_writer_version(name, ver): doc = WriterFactory.doc(name) WriterFactory.unregister(name) WriterFactory.register(name, doc)(WriterFactory.get_class(f'{name}_v{ver}')) + + +def active_writer_version(name): + """DEBUGGING TOOL to switch the "default" writer implementation""" + from pyomo.opt import WriterFactory + + ref = WriterFactory.get_class(name) + ver = 1 + try: + while 1: + if WriterFactory.get_class(f'{name}_v{ver}') is ref: + return ver + ver += 1 + except KeyError: + return None diff --git a/pyomo/repn/tests/test_plugins.py b/pyomo/repn/tests/test_plugins.py new file mode 100644 index 00000000000..fa6026ea74e --- /dev/null +++ b/pyomo/repn/tests/test_plugins.py @@ -0,0 +1,50 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest + +from pyomo.opt import WriterFactory +from pyomo.repn.plugins import activate_writer_version, active_writer_version + +import pyomo.environ + + +class TestPlugins(unittest.TestCase): + def test_active(self): + with self.assertRaises(KeyError): + active_writer_version('nonexistant_writer') + ver = active_writer_version('lp') + self.assertIs( + WriterFactory.get_class('lp'), WriterFactory.get_class(f'lp_v{ver}') + ) + + class TMP(object): + pass + + WriterFactory.register('test_writer')(TMP) + try: + self.assertIsNone(active_writer_version('test_writer')) + finally: + WriterFactory.unregister('test_writer') + + def test_activate(self): + ver = active_writer_version('lp') + try: + activate_writer_version('lp', 2) + self.assertIs( + WriterFactory.get_class('lp'), WriterFactory.get_class(f'lp_v2') + ) + activate_writer_version('lp', 1) + self.assertIs( + WriterFactory.get_class('lp'), WriterFactory.get_class(f'lp_v1') + ) + finally: + activate_writer_version('lp', ver) From 658dab36b72f3bd18759dad286f5e20b4ec936e7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 13:29:54 -0600 Subject: [PATCH 25/25] NFC: fix typo --- pyomo/repn/tests/test_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/tests/test_plugins.py b/pyomo/repn/tests/test_plugins.py index fa6026ea74e..1152131f6b6 100644 --- a/pyomo/repn/tests/test_plugins.py +++ b/pyomo/repn/tests/test_plugins.py @@ -20,7 +20,7 @@ class TestPlugins(unittest.TestCase): def test_active(self): with self.assertRaises(KeyError): - active_writer_version('nonexistant_writer') + active_writer_version('nonexistent_writer') ver = active_writer_version('lp') self.assertIs( WriterFactory.get_class('lp'), WriterFactory.get_class(f'lp_v{ver}')