From 7da1868771fe25f501d811707856eb43c86c2a43 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 29 Sep 2023 13:54:20 -0600 Subject: [PATCH 1/6] This test failure persists in OSX 12.7 --- pyomo/common/tests/test_fileutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/common/tests/test_fileutils.py b/pyomo/common/tests/test_fileutils.py index 2aaebded6cd..ea9e9494571 100644 --- a/pyomo/common/tests/test_fileutils.py +++ b/pyomo/common/tests/test_fileutils.py @@ -235,6 +235,7 @@ def test_findfile(self): and ( platform.mac_ver()[0].startswith('10.16') or platform.mac_ver()[0].startswith('12.6') + or platform.mac_ver()[0].startswith('12.7') ), "find_library has known bugs in Big Sur/Monterey for Python<3.8", ) From 08886bfcde72cf097b28249df0b405adca8cb3eb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 3 Oct 2023 11:38:54 -0600 Subject: [PATCH 2/6] Add a custom ASL solver for Gurobi (to implement license checks) --- pyomo/solvers/plugins/solvers/GUROBI.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/GUROBI.py b/pyomo/solvers/plugins/solvers/GUROBI.py index 45a44ac968b..cff2d5003ef 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI.py +++ b/pyomo/solvers/plugins/solvers/GUROBI.py @@ -19,6 +19,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch from pyomo.common.fileutils import this_file_dir +from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver @@ -32,8 +33,10 @@ ) from pyomo.opt.solver import ILMLicensedSystemCallSolver from pyomo.core.kernel.block import IBlock +from pyomo.core import ConcreteModel, Var, Objective from .gurobi_direct import gurobipy_available +from .ASL import ASL logger = logging.getLogger('pyomo.solvers') @@ -69,7 +72,7 @@ def __new__(cls, *args, **kwds): if mode == 'os': opt = SolverFactory('_ossolver', **kwds) elif mode == 'nl': - opt = SolverFactory('asl', **kwds) + opt = SolverFactory('_gurobi_nl', **kwds) else: logger.error('Unknown IO type: %s' % mode) return @@ -77,6 +80,21 @@ def __new__(cls, *args, **kwds): return opt +@SolverFactory.register('_gurobi_nl', doc='NL interface to the Gurobi solver') +class GUROBINL(ASL): + """NL interface to gurobi_ampl.""" + def license_is_valid(self): + m = ConcreteModel() + m.x = Var(bounds=(0,1)) + m.obj = Objective(expr=m.x) + try: + with capture_output(): + self.solve(m) + return m.x.value == 0 + except: + return False + + @SolverFactory.register( '_gurobi_shell', doc='Shell interface to the GUROBI LP/MIP solver' ) From 2de9ef6d68b577beff259c480bb904690589747f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 3 Oct 2023 12:57:00 -0600 Subject: [PATCH 3/6] Learning to spell because --- pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py | 2 +- pyomo/gdp/tests/test_partition_disjuncts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py index 55b359d5990..11c008acf82 100644 --- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py @@ -771,7 +771,7 @@ def test_integer_arithmetic_non1_coefficients(self): def test_numerical_instability_almost_canceling(self): # It's possible that we get almost-but-not-quite zero on the variable # being eliminated when we are doing this with floating point - # arithmetic. This can get ugly later becuase it might get muliplied by + # arithmetic. This can get ugly later because it might get muliplied by # a large number later and start to "reappear" m = ConcreteModel() m.x = Var() diff --git a/pyomo/gdp/tests/test_partition_disjuncts.py b/pyomo/gdp/tests/test_partition_disjuncts.py index 38e7ae19676..56faaa9b8f5 100644 --- a/pyomo/gdp/tests/test_partition_disjuncts.py +++ b/pyomo/gdp/tests/test_partition_disjuncts.py @@ -455,7 +455,7 @@ def test_transformation_block_nested_disjunction_outer_disjunction_target(self): self.check_transformation_block_nested_disjunction(m, disj2, m.disj1.x) def test_transformation_block_nested_disjunction_badly_ordered_targets(self): - """This tests that we preprocess targets correctly becuase we don't + """This tests that we preprocess targets correctly because we don't want to double transform the inner disjunct, which is what would happen if we did things in the order given.""" m = models.makeBetweenStepsPaperExample_Nested() From 5e368351c964cc31e362ffc228347509a695892f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 3 Oct 2023 13:25:40 -0600 Subject: [PATCH 4/6] Update Gurobi availability checks to verify license validity --- .../cp/tests/test_logical_to_disjunctive.py | 5 +++- pyomo/contrib/gdpopt/tests/test_enumerate.py | 29 ++++++++++--------- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 19 +++++++----- .../piecewise/tests/test_inner_repn_gdp.py | 1 + .../piecewise/tests/test_outer_repn_gdp.py | 7 ++--- .../tests/test_reduced_inner_repn.py | 1 + pyomo/gdp/tests/test_mbigm.py | 5 +++- .../tests/checks/test_gurobi_direct.py | 4 +++ 8 files changed, 43 insertions(+), 28 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py b/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py index 9cd5b2556ca..0437b92012c 100755 --- a/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py +++ b/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py @@ -45,7 +45,10 @@ TransformationFactory, ) -gurobi_available = SolverFactory('gurobi').available(exception_flag=False) +gurobi_available = ( + SolverFactory('gurobi').available(exception_flag=False) and + SolverFactory('gurobi').license_is_valid() +) class TestLogicalToDisjunctiveVisitor(unittest.TestCase): diff --git a/pyomo/contrib/gdpopt/tests/test_enumerate.py b/pyomo/contrib/gdpopt/tests/test_enumerate.py index 7f52f2f5c09..247e0a8bbc4 100644 --- a/pyomo/contrib/gdpopt/tests/test_enumerate.py +++ b/pyomo/contrib/gdpopt/tests/test_enumerate.py @@ -26,8 +26,8 @@ from pyomo.gdp import Disjunction import pyomo.gdp.tests.models as models - @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi not available') +@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'Gurobi not licensed') class TestGDPoptEnumerate(unittest.TestCase): def test_solve_two_term_disjunction(self): m = models.makeTwoTermDisj() @@ -144,18 +144,6 @@ def test_stop_at_iteration_limit(self): results.solver.termination_condition, TerminationCondition.maxIterations ) - @unittest.skipUnless(SolverFactory('ipopt').available(), 'Ipopt not available') - def test_infeasible_GDP(self): - m = models.make_infeasible_gdp_model() - - results = SolverFactory('gdpopt.enumerate').solve(m) - - self.assertEqual(results.solver.iterations, 2) - self.assertEqual( - results.solver.termination_condition, TerminationCondition.infeasible - ) - self.assertEqual(results.problem.lower_bound, float('inf')) - def test_unbounded_GDP(self): m = ConcreteModel() m.x = Var(bounds=(-1, 10)) @@ -173,7 +161,20 @@ def test_unbounded_GDP(self): self.assertEqual(results.problem.lower_bound, -float('inf')) self.assertEqual(results.problem.upper_bound, -float('inf')) - @unittest.skipUnless(SolverFactory('ipopt').available(), 'Ipopt not available') + +@unittest.skipUnless(SolverFactory('ipopt').available(), 'Ipopt not available') +class TestGDPoptEnumerate_ipopt_tests(unittest.TestCase): + def test_infeasible_GDP(self): + m = models.make_infeasible_gdp_model() + + results = SolverFactory('gdpopt.enumerate').solve(m) + + self.assertEqual(results.solver.iterations, 2) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + self.assertEqual(results.problem.lower_bound, float('inf')) + def test_algorithm_specified_to_solve(self): m = models.twoDisj_twoCircles_easy() diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 9fab71307a3..7f41be8010f 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -72,6 +72,11 @@ else False ) +gurobi_available = ( + SolverFactory('gurobi').available(exception_flag=False) and + SolverFactory('gurobi').license_is_valid() +) + class TestGDPoptUnit(unittest.TestCase): """Real unit tests for GDPopt""" @@ -129,7 +134,7 @@ def test_solve_lp(self): self.assertAlmostEqual(results.problem.lower_bound, 1) self.assertAlmostEqual(results.problem.upper_bound, 1) - @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi not available') + @unittest.skipUnless(gurobi_available, 'Gurobi not available') def test_solve_nlp(self): m = ConcreteModel() m.x = Var(bounds=(-5, 5)) @@ -222,7 +227,7 @@ def test_is_feasible_function(self): with self.assertRaisesRegex(NotImplementedError, "Found active disjunct"): is_feasible(m, GDP_LOA_Solver.CONFIG()) - @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi not available') + @unittest.skipUnless(gurobi_available, 'Gurobi not available') def test_infeasible_or_unbounded_mip_termination(self): m = ConcreteModel() m.x = Var() @@ -461,9 +466,7 @@ def test_unbounded_gdp_maximization(self): # [ESJ 5/16/22]: Using Gurobi for this test because glpk seems to get angry # on Windows when the MIP is arbitrarily bounded with the large bounds. And # I think I blame glpk... - @unittest.skipUnless( - SolverFactory('gurobi').available(), "Gurobi solver not available" - ) + @unittest.skipUnless(gurobi_available, "Gurobi solver not available") def test_GDP_nonlinear_objective(self): m = ConcreteModel() m.x = Var(bounds=(-1, 10)) @@ -1672,7 +1675,7 @@ def make_model(self): return m @unittest.skipIf(not mcpp_available(), "MC++ is not available") - @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi not available') + @unittest.skipUnless(gurobi_available, 'Gurobi not available') def test_set_options_on_config_block(self): m = self.make_model() @@ -1711,7 +1714,7 @@ def test_set_options_on_config_block(self): self.assertAlmostEqual(value(m.obj), -0.25) @unittest.skipIf(not mcpp_available(), "MC++ is not available") - @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi not available') + @unittest.skipUnless(gurobi_available, 'Gurobi not available') def test_set_options_in_init(self): m = self.make_model() @@ -1735,7 +1738,7 @@ def test_set_options_in_init(self): self.assertAlmostEqual(value(m.obj), -0.25) self.assertEqual(opt.config.mip_solver, 'gurobi') - @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi not available') + @unittest.skipUnless(gurobi_available, 'Gurobi not available') def test_no_default_algorithm(self): m = self.make_model() diff --git a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py index bba85a6bd7b..a0dbd1cca19 100644 --- a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py @@ -167,6 +167,7 @@ def test_descend_into_expressions_objective_target(self): ) @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_disaggregated_convex_combo_model(self): m = models.make_log_x_model() TransformationFactory( diff --git a/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py index d1b600075d2..edc5d9d3d95 100644 --- a/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py @@ -171,10 +171,9 @@ def test_descend_into_expressions_objective_target(self): self, 'contrib.piecewise.outer_repn_gdp' ) - @unittest.skipUnless( - SolverFactory('gurobi').available() and scipy_available, - 'Gurobi and/or scipy is not available', - ) + @unittest.skipUnless(scipy_available, "scipy is not available") + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_multiple_choice_model(self): m = models.make_log_x_model() TransformationFactory('contrib.piecewise.multiple_choice').apply_to(m) diff --git a/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py b/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py index b10edaac737..a2d41c04016 100644 --- a/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py +++ b/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py @@ -248,6 +248,7 @@ def test_descend_into_expressions_objective_target(self): ) @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_convex_combo_model(self): m = models.make_log_x_model() TransformationFactory('contrib.piecewise.convex_combination').apply_to(m) diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index 9c400dfcd29..b4a03bc83fe 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -41,7 +41,10 @@ ) from pyomo.repn import generate_standard_repn -gurobi_available = SolverFactory('gurobi').available() +gurobi_available = ( + SolverFactory('gurobi').available(exception_flag=False) and + SolverFactory('gurobi').license_is_valid() +) exdir = normpath(join(PYOMO_ROOT_DIR, 'examples', 'gdp')) diff --git a/pyomo/solvers/tests/checks/test_gurobi_direct.py b/pyomo/solvers/tests/checks/test_gurobi_direct.py index 8fb9526195d..43a5d9effed 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_direct.py +++ b/pyomo/solvers/tests/checks/test_gurobi_direct.py @@ -22,6 +22,7 @@ except ImportError: gurobipy_available = False +gurobi_available = GurobiDirect().available(exception_flag=False) def clean_up_global_state(): # Best efforts to dispose any gurobipy objects from previous tests @@ -79,6 +80,7 @@ def test_gurobipy_not_installed(self): @unittest.skipIf(not gurobipy_available, "gurobipy is not available") +@unittest.skipIf(not gurobi_available, "gurobi license is not valid") class GurobiParameterTests(GurobiBase): # Test parameter handling at the model and environment level @@ -158,6 +160,7 @@ def test_param_changes_4(self): @unittest.skipIf(not gurobipy_available, "gurobipy is not available") +@unittest.skipIf(not gurobi_available, "gurobi license is not valid") class GurobiEnvironmentTests(GurobiBase): # Test handling of gurobi environments @@ -318,6 +321,7 @@ def test_nonmanaged_env(self): @unittest.skipIf(not gurobipy_available, "gurobipy is not available") +@unittest.skipIf(not gurobi_available, "gurobi license is not valid") @unittest.skipIf(not single_use_license(), reason="test needs a single use license") class GurobiSingleUseTests(GurobiBase): # Integration tests for Gurobi single-use licenses (useful for checking all Gurobi From 6f9b2b0c4cfa6506861eca30f02a8bb0164d80e4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 3 Oct 2023 14:26:56 -0600 Subject: [PATCH 5/6] Apply black --- pyomo/contrib/cp/tests/test_logical_to_disjunctive.py | 4 ++-- pyomo/contrib/gdpopt/tests/test_enumerate.py | 1 + pyomo/contrib/gdpopt/tests/test_gdpopt.py | 4 ++-- pyomo/gdp/tests/test_mbigm.py | 4 ++-- pyomo/solvers/plugins/solvers/GUROBI.py | 3 ++- pyomo/solvers/tests/checks/test_gurobi_direct.py | 1 + 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py b/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py index 0437b92012c..c6733f34f83 100755 --- a/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py +++ b/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py @@ -46,8 +46,8 @@ ) gurobi_available = ( - SolverFactory('gurobi').available(exception_flag=False) and - SolverFactory('gurobi').license_is_valid() + SolverFactory('gurobi').available(exception_flag=False) + and SolverFactory('gurobi').license_is_valid() ) diff --git a/pyomo/contrib/gdpopt/tests/test_enumerate.py b/pyomo/contrib/gdpopt/tests/test_enumerate.py index 247e0a8bbc4..606dd172064 100644 --- a/pyomo/contrib/gdpopt/tests/test_enumerate.py +++ b/pyomo/contrib/gdpopt/tests/test_enumerate.py @@ -26,6 +26,7 @@ from pyomo.gdp import Disjunction import pyomo.gdp.tests.models as models + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi not available') @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'Gurobi not licensed') class TestGDPoptEnumerate(unittest.TestCase): diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 7f41be8010f..1d5559a9b33 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -73,8 +73,8 @@ ) gurobi_available = ( - SolverFactory('gurobi').available(exception_flag=False) and - SolverFactory('gurobi').license_is_valid() + SolverFactory('gurobi').available(exception_flag=False) + and SolverFactory('gurobi').license_is_valid() ) diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index b4a03bc83fe..1e10b098664 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -42,8 +42,8 @@ from pyomo.repn import generate_standard_repn gurobi_available = ( - SolverFactory('gurobi').available(exception_flag=False) and - SolverFactory('gurobi').license_is_valid() + SolverFactory('gurobi').available(exception_flag=False) + and SolverFactory('gurobi').license_is_valid() ) exdir = normpath(join(PYOMO_ROOT_DIR, 'examples', 'gdp')) diff --git a/pyomo/solvers/plugins/solvers/GUROBI.py b/pyomo/solvers/plugins/solvers/GUROBI.py index cff2d5003ef..f58f996cadd 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI.py +++ b/pyomo/solvers/plugins/solvers/GUROBI.py @@ -83,9 +83,10 @@ def __new__(cls, *args, **kwds): @SolverFactory.register('_gurobi_nl', doc='NL interface to the Gurobi solver') class GUROBINL(ASL): """NL interface to gurobi_ampl.""" + def license_is_valid(self): m = ConcreteModel() - m.x = Var(bounds=(0,1)) + m.x = Var(bounds=(0, 1)) m.obj = Objective(expr=m.x) try: with capture_output(): diff --git a/pyomo/solvers/tests/checks/test_gurobi_direct.py b/pyomo/solvers/tests/checks/test_gurobi_direct.py index 43a5d9effed..7c60b207a9f 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_direct.py +++ b/pyomo/solvers/tests/checks/test_gurobi_direct.py @@ -24,6 +24,7 @@ gurobi_available = GurobiDirect().available(exception_flag=False) + def clean_up_global_state(): # Best efforts to dispose any gurobipy objects from previous tests # which might keep the default environment active From c6354a297663bd02f1f6a9e809e0a9811614e4f4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 3 Oct 2023 16:36:01 -0600 Subject: [PATCH 6/6] Update license test to be more robust to integer precision --- pyomo/solvers/plugins/solvers/GUROBI.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/GUROBI.py b/pyomo/solvers/plugins/solvers/GUROBI.py index f58f996cadd..3c7c227b9e7 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI.py +++ b/pyomo/solvers/plugins/solvers/GUROBI.py @@ -86,12 +86,12 @@ class GUROBINL(ASL): def license_is_valid(self): m = ConcreteModel() - m.x = Var(bounds=(0, 1)) + m.x = Var(bounds=(1, 2)) m.obj = Objective(expr=m.x) try: with capture_output(): self.solve(m) - return m.x.value == 0 + return abs(m.x.value - 1) <= 1e-4 except: return False