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.*'), diff --git a/pyomo/common/tests/test_download.py b/pyomo/common/tests/test_download.py index 8fee0ba7e31..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, @@ -206,7 +207,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 diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index c78e003a07d..996bb69ec78 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -858,7 +858,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 diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 76cd204e36d..af40d2e88d2 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -567,3 +567,24 @@ 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/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/doe/doe.py b/pyomo/contrib/doe/doe.py index a120add4200..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. @@ -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 @@ -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 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_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index d9a8d60fdb4..9cae2fe6278 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, "ipopt is not available") class TestPriorFIMError(unittest.TestCase): def test(self): # Control time set [h] 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") diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index dca05026e80..3b0c869affa 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -12,9 +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( @@ -43,6 +45,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 ( @@ -66,11 +69,11 @@ def test_likelihood_ratio_example(self): likelihood_ratio_example.main() -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", +@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): @@ -140,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, diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 65e2e4a3b06..52b7cd390e8 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -9,43 +9,29 @@ # 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__)) +pynumero_ASL_available = AmplInterface.available() +testdir = this_file_dir() @unittest.skipIf( @@ -208,17 +194,7 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) self.assertEqual(retcode, 0) - @unittest.skip("Most folks don't have k_aug installed") - 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 +544,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 +568,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 +582,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 +599,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 +616,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 +899,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 +1194,7 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) assert retcode == 0 - @unittest.skip("Most folks don't have k_aug installed") - 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 +1452,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 +1481,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 +1513,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 +1545,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 +1889,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, 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/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index cc459245506..27a80feede0 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', @@ -167,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/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( diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 0361f41c835..8f3fab23bbb 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() diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index d3804c55106..4029f44a03d 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -37,6 +37,23 @@ 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}')) + + +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..1152131f6b6 --- /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('nonexistent_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) diff --git a/pyomo/scripting/driver_help.py b/pyomo/scripting/driver_help.py index 38d1a4c16bf..45fdc711137 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,44 @@ 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 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 getattr(opt, "_metasolver", False): + # Note: 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) 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 diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 21045cb7b4f..82dcfdb75a0 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -14,6 +14,8 @@ 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 from pyomo.opt.base import ProblemFormat, ResultsFormat @@ -207,3 +209,16 @@ 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) + 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')) 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):