From c9ed4a6c81a5b738ab0796ef2f90fee93ba68e66 Mon Sep 17 00:00:00 2001 From: "David E. Bernal Neira" Date: Mon, 11 May 2026 09:36:44 -0400 Subject: [PATCH 1/2] Fix GDPopt LBB time-limit results (#3941) --- pyomo/contrib/gdpopt/branch_and_bound.py | 2 +- pyomo/contrib/gdpopt/tests/test_LBB.py | 47 ++++++++++++++++++- .../mindtpy/tests/test_mindtpy_no_discrete.py | 22 ++++++++- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/gdpopt/branch_and_bound.py b/pyomo/contrib/gdpopt/branch_and_bound.py index afabdc39123..7cf76ece553 100644 --- a/pyomo/contrib/gdpopt/branch_and_bound.py +++ b/pyomo/contrib/gdpopt/branch_and_bound.py @@ -236,7 +236,7 @@ def _solve_gdp(self, model, config): config.logger.info( 'Final bound values: LB: {} UB: {}'.format(self.LB, self.UB) ) - return self._get_final_results_object() + return self._get_final_pyomo_results_object() # Handle current node if not node_data.is_screened: diff --git a/pyomo/contrib/gdpopt/tests/test_LBB.py b/pyomo/contrib/gdpopt/tests/test_LBB.py index 871b79ecc31..e52932c0a70 100644 --- a/pyomo/contrib/gdpopt/tests/test_LBB.py +++ b/pyomo/contrib/gdpopt/tests/test_LBB.py @@ -20,8 +20,17 @@ from pyomo.common.log import LoggingIntercept import pyomo.contrib.gdpopt.tests.common_tests as ct from pyomo.contrib.satsolver.satsolver import z3_available -from pyomo.environ import SolverFactory, value, ConcreteModel, Var, Objective, maximize -from pyomo.gdp import Disjunction +from pyomo.contrib.gdpopt.branch_and_bound import GDP_LBB_Solver +from pyomo.environ import ( + SolverFactory, + value, + ConcreteModel, + Constraint, + Var, + Objective, + maximize, +) +from pyomo.gdp import Disjunct, Disjunction from pyomo.opt import TerminationCondition currdir = dirname(abspath(__file__)) @@ -35,6 +44,40 @@ ) +class TestGDPopt_LBB_TimeLimit(unittest.TestCase): + """Tests for solver-independent LBB termination paths.""" + + def test_time_limit_returns_pyomo_results_object(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 2)) + m.d1 = Disjunct() + m.d2 = Disjunct() + m.d1.c = Constraint(expr=m.x <= 0.5) + m.d2.c = Constraint(expr=m.x >= 1.5) + m.disj = Disjunction(expr=[m.d1, m.d2]) + m.obj = Objective(expr=m.x) + + orig_reached_time_limit = GDP_LBB_Solver.reached_time_limit + + def force_time_limit(solver, config): + solver.pyomo_results.solver.termination_condition = ( + TerminationCondition.maxTimeLimit + ) + return True + + GDP_LBB_Solver.reached_time_limit = force_time_limit + try: + results = SolverFactory('gdpopt.lbb').solve(m, time_limit=1, tee=False) + finally: + GDP_LBB_Solver.reached_time_limit = orig_reached_time_limit + + self.assertEqual( + results.solver.termination_condition, TerminationCondition.maxTimeLimit + ) + self.assertEqual(results.problem.lower_bound, float('-inf')) + self.assertEqual(results.problem.upper_bound, float('inf')) + + @unittest.skipUnless( solver_available, "Required subsolver %s is not available" % (minlp_solver,) ) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py index 1ba46b08d75..a1ddd4bec8d 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py @@ -10,7 +10,8 @@ from unittest.mock import MagicMock, patch -from pyomo.opt import TerminationCondition as tc, SolverStatus +from pyomo.common import timing +from pyomo.opt import TerminationCondition as tc, SolverStatus, SolverResults import pyomo.common.unittest as unittest from pyomo.environ import ( @@ -455,6 +456,25 @@ def test_solver_status_and_message_mirrored(self): self.assertEqual(algo.results.solver.message, "All good") +class TestMindtPyGOATimeLimit(unittest.TestCase): + def test_goa_time_limit_sets_solver_results_condition(self): + from pyomo.contrib.mindtpy.global_outer_approximation import MindtPy_GOA_Solver + + solver = MindtPy_GOA_Solver() + solver.config = _SimpleNamespace( + logger=MagicMock(), single_tree=False, time_limit=1 + ) + solver.results = SolverResults() + solver.timing = _SimpleNamespace( + main_timer_start_time=timing.default_timer() - 2 + ) + solver.primal_bound = float('inf') + solver.dual_bound = float('-inf') + + self.assertTrue(solver.reached_time_limit()) + self.assertEqual(solver.results.solver.termination_condition, tc.maxTimeLimit) + + class _FakeLegacyMIPSolver: def __init__( self, From ede4b3787c281d480ed80e4fdcbce012b4241490 Mon Sep 17 00:00:00 2001 From: "David E. Bernal Neira" Date: Tue, 12 May 2026 00:16:06 -0400 Subject: [PATCH 2/2] Address MindtPy GOA test import review --- pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py index a1ddd4bec8d..7019f2db2ab 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_no_discrete.py @@ -11,6 +11,7 @@ from unittest.mock import MagicMock, patch from pyomo.common import timing +from pyomo.contrib.mindtpy.global_outer_approximation import MindtPy_GOA_Solver from pyomo.opt import TerminationCondition as tc, SolverStatus, SolverResults import pyomo.common.unittest as unittest @@ -458,8 +459,6 @@ def test_solver_status_and_message_mirrored(self): class TestMindtPyGOATimeLimit(unittest.TestCase): def test_goa_time_limit_sets_solver_results_condition(self): - from pyomo.contrib.mindtpy.global_outer_approximation import MindtPy_GOA_Solver - solver = MindtPy_GOA_Solver() solver.config = _SimpleNamespace( logger=MagicMock(), single_tree=False, time_limit=1