From 05f8b0d32897f220495eb0ede2f4085930cc2bf6 Mon Sep 17 00:00:00 2001 From: xueyuan Date: Fri, 31 Jul 2026 10:42:07 +0800 Subject: [PATCH] Support trivial constraints in SCIP interfaces --- pyomo/contrib/solver/solvers/scip/base.py | 8 +++++++ .../solver/tests/solvers/test_scip_direct.py | 20 ++++++++++++++++++ .../tests/solvers/test_scip_persistent.py | 21 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/pyomo/contrib/solver/solvers/scip/base.py b/pyomo/contrib/solver/solvers/scip/base.py index 73bf69e1528..c1519fa60ac 100644 --- a/pyomo/contrib/solver/solvers/scip/base.py +++ b/pyomo/contrib/solver/solvers/scip/base.py @@ -46,6 +46,7 @@ EqualityExpression, InequalityExpression, RangedExpression, + TrivialRelationalExpression, ) from pyomo.core.expr.visitor import StreamBasedExpressionVisitor from pyomo.gdp.disjunct import AutoLinkedBinaryVar @@ -218,6 +219,12 @@ def _handle_inequality(node, data, opt, visitor): return data[0] <= data[1] +def _handle_trivial_inequality(node, data, opt, visitor): + # Keep the constant relation symbolic so that PySCIPOpt creates a + # constraint instead of Python evaluating it to a bool. + return scip.Expr() + data[0] <= data[1] + + def _handle_named_expression(node, data, opt, visitor): return data[0] @@ -244,6 +251,7 @@ def _handle_unit(node, data, opt, visitor): EqualityExpression: _handle_equality, RangedExpression: _handle_ranged, InequalityExpression: _handle_inequality, + TrivialRelationalExpression: _handle_trivial_inequality, ScalarExpression: _handle_named_expression, ExpressionData: _handle_named_expression, VarData: _handle_var, diff --git a/pyomo/contrib/solver/tests/solvers/test_scip_direct.py b/pyomo/contrib/solver/tests/solvers/test_scip_direct.py index 221039db764..ce73b976ba3 100644 --- a/pyomo/contrib/solver/tests/solvers/test_scip_direct.py +++ b/pyomo/contrib/solver/tests/solvers/test_scip_direct.py @@ -243,6 +243,26 @@ def test_infeasible_raises_no_feasible_solution_error(self): m, load_solutions=True, raise_exception_on_nonoptimal_result=False ) + def test_trivial_constraints(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(0, 1)) + m.feasible = pyo.Constraint(expr=pyo.Constraint.Feasible) + m.obj = pyo.Objective(expr=m.x) + + opt = ScipDirect() + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 0) + + m.infeasible = pyo.Constraint(expr=pyo.Constraint.Infeasible) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + self.assertEqual( + res.termination_condition, TerminationCondition.provenInfeasible + ) + self.assertEqual(res.solution_status, SolutionStatus.noSolution) + def test_timer(self): m = self.create_lp_model() timer = HierarchicalTimer() diff --git a/pyomo/contrib/solver/tests/solvers/test_scip_persistent.py b/pyomo/contrib/solver/tests/solvers/test_scip_persistent.py index aa17d5c3495..ba136721dba 100644 --- a/pyomo/contrib/solver/tests/solvers/test_scip_persistent.py +++ b/pyomo/contrib/solver/tests/solvers/test_scip_persistent.py @@ -289,3 +289,24 @@ def test_infeasible_raises(self): opt.solve( m, load_solutions=True, raise_exception_on_nonoptimal_result=False ) + + def test_trivial_constraints(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(0, 1)) + m.feasible = pyo.Constraint(expr=pyo.Constraint.Feasible) + m.obj = pyo.Objective(expr=m.x) + + opt = ScipPersistent() + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 0) + + m.infeasible = pyo.Constraint(expr=pyo.Constraint.Infeasible) + opt.add_constraints([m.infeasible]) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + self.assertEqual( + res.termination_condition, TerminationCondition.provenInfeasible + ) + self.assertEqual(res.solution_status, SolutionStatus.noSolution)