Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
36 changes: 36 additions & 0 deletions pyomo/contrib/solver/common/solution_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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


class SolutionLoader:
Expand Down Expand Up @@ -331,6 +332,41 @@ def load_import_suffixes(self):
return self._loader.load_import_suffixes()


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

def get_solution_ids(self) -> List[Any]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is redundant (covered by the base class)

return []

def get_number_of_solutions(self) -> int:
return 0

def load_solution(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is redundant (the base class will call get_vars(), which will raise the exception)

raise NoSolutionError()

def load_vars(self, vars_to_load: Sequence[VarData] | None = None) -> None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is redundant (the base class will call get_vars(), which will raise the exception)

raise NoSolutionError()

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

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

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

def load_import_suffixes(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this is redundant? If the model has dual / rc Suffixes, then the base class implementation will call get_duals() / get_reduced_costs(), which will raise the exception. If those suffixes aren't defined, then nothing will happen ... but that might be consistent with the behavior implied by the base class implementation?

raise NoSolutionError()


class PersistentSolutionLoader(SolutionLoader):
"""
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 @@ -34,6 +34,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 @@ -375,6 +376,8 @@ def solve(self, model, **kwds) -> Results:
has_obj=has_obj,
config=config,
)
except InfeasibleConstraintException:
res = self._get_infeasible_results(config=config)
Comment on lines +379 to +380
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This exception should have information about the infeasible constraint in its message. That should be preserved and added to the NoSolutionError / NoOptimalSolutionError / NoFeasibleSolutionError exceptions

finally:
os.chdir(orig_cwd)

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

def _get_infeasible_results(self, config):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be promoted to a standard method (in results / util) that is specialized here?

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 = config
res.solver_name = self.name
res.solver_version = self.version()
if config.raise_exception_on_nonoptimal_result:
raise NoOptimalSolutionError()
if config.load_solutions:
raise NoFeasibleSolutionError()
return res

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

Expand Down
1 change: 1 addition & 0 deletions pyomo/contrib/solver/solvers/gurobi/gurobi_persistent.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pyomo.common.errors import PyomoException
from pyomo.common.shutdown import python_is_shutting_down
from pyomo.common.timing import HierarchicalTimer
from pyomo.common.errors import InfeasibleConstraintException
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was this added? It appears to be unused.

from pyomo.core.base.objective import ObjectiveData
from pyomo.core.kernel.objective import minimize, maximize
from pyomo.core.base.var import VarData
Expand Down
52 changes: 52 additions & 0 deletions pyomo/contrib/solver/tests/solvers/test_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,58 @@ def test_results_infeasible(
):
res.solution_loader.get_reduced_costs()

@mark_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)

@mark_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 @@ -18,6 +18,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 @@ -460,7 +461,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