From b19576e7849d60d3336ea5af590f3f5573a69c6a Mon Sep 17 00:00:00 2001 From: "David E. Bernal Neira" Date: Sun, 10 May 2026 12:53:35 -0400 Subject: [PATCH 1/2] Fix GDPopt LOA maximization objective sense (#3937) --- pyomo/contrib/gdpopt/loa.py | 4 ++- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 43 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/gdpopt/loa.py b/pyomo/contrib/gdpopt/loa.py index 8e36bebfd22..1526e5dff6e 100644 --- a/pyomo/contrib/gdpopt/loa.py +++ b/pyomo/contrib/gdpopt/loa.py @@ -147,7 +147,9 @@ def _setup_augmented_penalty_objective(self, discrete_problem_util_block): # Set up augmented penalty objective discrete_objective.deactivate() # placeholder for OA objective - discrete_problem_util_block.oa_obj = Objective(sense=minimize) + discrete_problem_util_block.oa_obj = Objective( + expr=0, sense=discrete_objective.sense + ) return discrete_objective diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 5a0fc30cf37..68a1ebb4fe4 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -40,6 +40,7 @@ Integers, LogicalConstraint, maximize, + minimize, Objective, RangeSet, TransformationFactory, @@ -322,6 +323,48 @@ def test_complain_when_no_algorithm_specified(self): ): SolverFactory('gdpopt').solve(m) + def test_loa_augmented_penalty_objective_preserves_objective_sense(self): + for sense in (minimize, maximize): + m = ConcreteModel() + m.GDPopt_utils = Block() + m.x = Var(bounds=(0, 1)) + m.obj = Objective(expr=m.x, sense=sense) + + solver = SolverFactory('gdpopt.loa') + original_obj = solver._setup_augmented_penalty_objective(m.GDPopt_utils) + + self.assertIs(original_obj, m.obj) + self.assertFalse(m.obj.active) + self.assertEqual(m.GDPopt_utils.oa_obj.sense, sense) + + @unittest.skipUnless( + SolverFactory(mip_solver).available(), "MIP solver not available" + ) + def test_LOA_maximize_matches_minimize_negated_objective(self): + def build_model(maximize_objective): + m = ConcreteModel() + m.x = Var(bounds=(0, 10)) + m.disjunction = Disjunction(expr=[[m.x == 1], [m.x == 9]]) + if maximize_objective: + m.obj = Objective(expr=m.x, sense=maximize) + else: + m.obj = Objective(expr=-m.x) + return m + + for maximize_objective in (False, True): + m = build_model(maximize_objective) + results = SolverFactory('gdpopt.loa').solve( + m, + mip_solver=mip_solver, + nlp_solver=nlp_solver, + init_algorithm='no_init', + ) + + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + self.assertAlmostEqual(value(m.x), 9) + @unittest.skipIf( not LOA_solvers_available, "Required subsolvers %s are not available" % (LOA_solvers,), From 326258353c04f81ad49d31add5b954069fb13901 Mon Sep 17 00:00:00 2001 From: "David E. Bernal Neira" Date: Tue, 12 May 2026 00:04:43 -0400 Subject: [PATCH 2/2] Remove dummy LOA objective expression --- pyomo/contrib/gdpopt/loa.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/gdpopt/loa.py b/pyomo/contrib/gdpopt/loa.py index 1526e5dff6e..3f9db839b93 100644 --- a/pyomo/contrib/gdpopt/loa.py +++ b/pyomo/contrib/gdpopt/loa.py @@ -147,9 +147,8 @@ def _setup_augmented_penalty_objective(self, discrete_problem_util_block): # Set up augmented penalty objective discrete_objective.deactivate() # placeholder for OA objective - discrete_problem_util_block.oa_obj = Objective( - expr=0, sense=discrete_objective.sense - ) + discrete_problem_util_block.oa_obj = Objective() + discrete_problem_util_block.oa_obj.sense = discrete_objective.sense return discrete_objective