Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d16bee5
adding tests for trivial constraints and fixing bugs
michaelbynum Aug 14, 2025
2001d15
Merge branch 'solver_api' into trivial_constraints
michaelbynum Aug 14, 2025
d25e721
run black
michaelbynum Aug 14, 2025
02f383d
Merge branch 'solver_api' into trivial_constraints
michaelbynum Aug 16, 2025
3d302a1
Merge branch 'solver_api' into trivial_constraints
michaelbynum Aug 16, 2025
ecd602d
Merge branch 'solver_api' into trivial_constraints
michaelbynum Aug 18, 2025
92e77ba
merge solver_api into trivial_constraints
michaelbynum Oct 5, 2025
045f537
merge main
michaelbynum Dec 12, 2025
253bdd9
Merge branch 'solver_api' into trivial_constraints
michaelbynum Dec 18, 2025
c3ad9b5
Merge branch 'solver_api' into trivial_constraints
michaelbynum Jan 29, 2026
ef6dcf6
Merge branch 'solver_api' into trivial_constraints
michaelbynum Feb 13, 2026
fb38368
Merge branch 'solver_api' into trivial_constraints
michaelbynum Feb 13, 2026
0365ae5
merge solver_api into trivial_constraints
michaelbynum Mar 23, 2026
7293c29
Merge branch 'solver_api' into trivial_constraints
michaelbynum Mar 23, 2026
75fee85
Merge branch 'solver_api' into trivial_constraints
michaelbynum Mar 26, 2026
29a72f7
Merge branch 'solver_api' into trivial_constraints
michaelbynum Apr 9, 2026
c216e19
Merge branch 'solver_api' into trivial_constraints
michaelbynum Apr 15, 2026
b754a29
merge main
michaelbynum Apr 27, 2026
ae7d2d9
run black
michaelbynum Apr 27, 2026
87a0f75
Merge remote-tracking branch 'origin/main' into trivial_constraints
michaelbynum May 1, 2026
d0c0515
remove redundant code and propagate error messages
michaelbynum May 1, 2026
bf6f6fd
fix highs interface
michaelbynum May 1, 2026
a1ed8e0
better handling of trivially feasible and infeasible constraints
michaelbynum May 3, 2026
29b9bf3
run black
michaelbynum May 3, 2026
eb7abb6
better handling of trivially infeasible constraints
michaelbynum May 3, 2026
c987a9f
run black
michaelbynum May 3, 2026
fe00fdd
better handling of trivially infeasible constraints
michaelbynum May 3, 2026
0c54635
remove unused line
michaelbynum May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyomo/contrib/observer/model_observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ def _check_for_var_changes(self):
vars_to_update.append(v)
elif _domain_interval != v.domain.get_interval():
vars_to_update.append(v)
elif v.value != _value:
elif v.fixed and v.value != _value:
vars_to_update.append(v)
cons_to_update = list(cons_to_update.keys())
return vars_to_update, cons_to_update, update_obj
Expand Down
38 changes: 38 additions & 0 deletions pyomo/contrib/solver/common/solution_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pyomo.core.base.var import VarData
from pyomo.core.staleflag import StaleFlagManager
from pyomo.core.base.suffix import Suffix
from .util import NoSolutionError


def load_import_suffixes(
Expand Down Expand Up @@ -187,6 +188,43 @@ def load_import_suffixes(self, solution_id=None):
return NotImplemented


class NoSolutionSolutionLoader(SolutionLoaderBase):
def __init__(self) -> None:
pass

def get_solution_ids(self) -> List[Any]:
Comment thread
michaelbynum marked this conversation as resolved.
Outdated
return []

def get_number_of_solutions(self) -> int:
return 0

def load_solution(self, solution_id=None):
raise NoSolutionError()

def load_vars(
self, vars_to_load: Sequence[VarData] | None = None, solution_id=None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use Optional here?

) -> None:
raise NoSolutionError()

def get_vars(
self, vars_to_load: Sequence[VarData] | None = None, solution_id=None
) -> Mapping[VarData, float]:
raise NoSolutionError()

def get_duals(
self, cons_to_load: Sequence[ConstraintData] | None = None, solution_id=None
) -> Dict[ConstraintData, float]:
raise NoSolutionError()

def get_reduced_costs(
self, vars_to_load: Sequence[VarData] | None = None, solution_id=None
) -> Mapping[VarData, float]:
raise NoSolutionError()

def load_import_suffixes(self, solution_id=None):
raise NoSolutionError()


class PersistentSolutionLoader(SolutionLoaderBase):
"""
Loader for persistent solvers
Expand Down
23 changes: 22 additions & 1 deletion pyomo/contrib/solver/solvers/gurobi/gurobi_direct_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from pyomo.common.config import ConfigValue
from pyomo.common.dependencies import attempt_import
from pyomo.common.enums import ObjectiveSense
from pyomo.common.errors import ApplicationError
from pyomo.common.errors import ApplicationError, InfeasibleConstraintException
from pyomo.common.shutdown import python_is_shutting_down
from pyomo.common.tee import capture_output, TeeStream
from pyomo.common.timing import HierarchicalTimer
Expand All @@ -33,6 +33,7 @@
NoReducedCostsError,
NoSolutionError,
)
from pyomo.contrib.solver.common.solution_loader import NoSolutionSolutionLoader
from pyomo.contrib.solver.common.results import (
Results,
SolutionStatus,
Expand Down Expand Up @@ -353,6 +354,8 @@ def solve(self, model, **kwds) -> Results:
res = self._postsolve(
grb_model=gurobi_model, solution_loader=solution_loader, has_obj=has_obj
)
except InfeasibleConstraintException:
res = self._get_infeasible_results()
finally:
os.chdir(orig_cwd)

Expand Down Expand Up @@ -390,6 +393,24 @@ def _get_tc_map(self):
}
return GurobiDirectBase._tc_map

def _get_infeasible_results(self):
res = Results()
res.solution_loader = NoSolutionSolutionLoader()
res.solution_status = SolutionStatus.noSolution
res.termination_condition = TerminationCondition.provenInfeasible
res.incumbent_objective = None
res.objective_bound = None
res.iteration_count = None
res.timing_info.gurobi_time = None
res.solver_config = self.config
res.solver_name = self.name
res.solver_version = self.version()
if self.config.raise_exception_on_nonoptimal_result:
raise NoOptimalSolutionError()
if self.config.load_solutions:
raise NoFeasibleSolutionError()
return res

def _postsolve(self, grb_model, solution_loader, has_obj):
status = grb_model.Status

Expand Down
6 changes: 5 additions & 1 deletion pyomo/contrib/solver/solvers/gurobi/gurobi_persistent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pyomo.common.collections import ComponentSet, OrderedSet
from pyomo.common.shutdown import python_is_shutting_down
from pyomo.common.timing import HierarchicalTimer
from pyomo.common.errors import InfeasibleConstraintException
Comment thread
michaelbynum marked this conversation as resolved.
Outdated
from pyomo.core.base.objective import ObjectiveData
from pyomo.core.kernel.objective import minimize, maximize
from pyomo.core.base.var import VarData
Expand Down Expand Up @@ -487,7 +488,9 @@ def _get_expr_from_pyomo_repn(self, repn):
vlist = [self._pyomo_var_to_solver_var_map[id(v)] for v in repn.linear_vars]
new_expr = gurobipy.LinExpr(coef_list, vlist)
else:
new_expr = 0.0
# this can't just be zero in case the constraint is a
# trivial one
new_expr = gurobipy.LinExpr()

if len(repn.quadratic_vars) > 0:
missing_vars = {}
Expand Down Expand Up @@ -737,6 +740,7 @@ def _add_constraints(self, cons: List[ConstraintData]):
for ndx, con in enumerate(cons):
lb, body, ub = con.to_bounded_expression(evaluate_bounds=False)
repn = generate_standard_repn(body, quadratic=True, compute_values=False)

if len(repn.quadratic_vars) > 0:
self._quadratic_cons.add(con)
else:
Expand Down
57 changes: 57 additions & 0 deletions pyomo/contrib/solver/tests/solvers/test_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
GurobiDirectQuadratic,
GurobiPersistent,
)
from pyomo.contrib.solver.common.util import (
NoSolutionError,
NoFeasibleSolutionError,
NoOptimalSolutionError,
)
from pyomo.contrib.solver.solvers.highs import Highs
from pyomo.core.expr.numeric_expr import LinearExpression
from pyomo.core.expr.compare import assertExpressionsEqual
Expand Down Expand Up @@ -1053,6 +1058,58 @@ def test_results_infeasible(
):
res.solution_loader.get_reduced_costs()

@parameterized.expand(input=_load_tests(all_solvers))
def test_trivial_constraints(
self, name: str, opt_class: Type[SolverBase], use_presolve: bool
):
opt: SolverBase = opt_class()
if not opt.available():
raise unittest.SkipTest(f'Solver {opt.name} not available.')
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
opt.config.writer_config.linear_presolve = True
else:
opt.config.writer_config.linear_presolve = False
m = pyo.ConcreteModel()
m.x = pyo.Var()
m.y = pyo.Var()
m.obj = pyo.Objective(expr=m.y)
m.c1 = pyo.Constraint(expr=m.y >= m.x)
m.c2 = pyo.Constraint(expr=m.y >= -m.x)
m.c3 = pyo.Constraint(expr=m.x >= 0)

res = opt.solve(m)
self.assertAlmostEqual(m.x.value, 0)
self.assertAlmostEqual(m.y.value, 0)

m.x.fix(1)
opt.config.tee = True
res = opt.solve(m)
self.assertAlmostEqual(m.x.value, 1)
self.assertAlmostEqual(m.y.value, 1)

m.x.fix(-1)
with self.assertRaises(NoOptimalSolutionError):
res = opt.solve(m)

opt.config.load_solutions = False
opt.config.raise_exception_on_nonoptimal_result = False
res = opt.solve(m)
self.assertNotEqual(res.solution_status, SolutionStatus.optimal)
if isinstance(opt, Ipopt):
acceptable_termination_conditions = {
TerminationCondition.locallyInfeasible,
TerminationCondition.unbounded,
TerminationCondition.provenInfeasible,
}
else:
acceptable_termination_conditions = {
TerminationCondition.provenInfeasible,
TerminationCondition.infeasibleOrUnbounded,
}
self.assertIn(res.termination_condition, acceptable_termination_conditions)
self.assertIsNone(res.incumbent_objective)

@parameterized.expand(input=_load_tests(all_solvers))
def test_duals(self, name: str, opt_class: Type[SolverBase], use_presolve: bool):
opt: SolverBase = opt_class()
Expand Down
3 changes: 2 additions & 1 deletion pyomo/repn/plugins/standard_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
InEnum,
document_kwargs_from_configdict,
)
from pyomo.common.errors import InfeasibleConstraintException
from pyomo.common.dependencies import scipy, numpy as np
from pyomo.common.enums import ObjectiveSense
from pyomo.common.gc_manager import PauseGC
Expand Down Expand Up @@ -462,7 +463,7 @@ def write(self, model):
# TODO: add a (configurable) feasibility tolerance
if (lb is None or lb <= offset) and (ub is None or ub >= offset):
continue
raise InfeasibleError(
raise InfeasibleConstraintException(
f"model contains a trivially infeasible constraint, '{con.name}'"
)

Expand Down
Loading