mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -06:00
Refactor StaticLazy; remove old constraint methods
This commit is contained in:
@@ -91,26 +91,6 @@ class GurobiSolver(InternalSolver):
|
||||
self.gp.GRB.Callback.MIPNODE,
|
||||
]
|
||||
|
||||
@overrides
|
||||
def add_constraint(self, constr: Constraint, name: str) -> None:
|
||||
assert self.model is not None
|
||||
assert self._varname_to_var is not None
|
||||
|
||||
assert constr.lhs is not None
|
||||
lhs = self.gp.quicksum(
|
||||
self._varname_to_var[varname] * coeff
|
||||
for (varname, coeff) in constr.lhs.items()
|
||||
)
|
||||
if constr.sense == "=":
|
||||
self.model.addConstr(lhs == constr.rhs, name=name)
|
||||
elif constr.sense == "<":
|
||||
self.model.addConstr(lhs <= constr.rhs, name=name)
|
||||
else:
|
||||
self.model.addConstr(lhs >= constr.rhs, name=name)
|
||||
self._dirty = True
|
||||
self._has_lp_solution = False
|
||||
self._has_mip_solution = False
|
||||
|
||||
@overrides
|
||||
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
||||
assert cf.names is not None
|
||||
@@ -143,7 +123,7 @@ class GurobiSolver(InternalSolver):
|
||||
self,
|
||||
cf: ConstraintFeatures,
|
||||
tol: float = 1e-5,
|
||||
) -> List[bool]:
|
||||
) -> Tuple[bool, ...]:
|
||||
assert cf.names is not None
|
||||
assert cf.senses is not None
|
||||
assert cf.lhs is not None
|
||||
@@ -162,7 +142,7 @@ class GurobiSolver(InternalSolver):
|
||||
result.append(lhs >= cf.rhs[i] - tol)
|
||||
else:
|
||||
result.append(abs(cf.rhs[i] - lhs) <= tol)
|
||||
return result
|
||||
return tuple(result)
|
||||
|
||||
@overrides
|
||||
def build_test_instance_infeasible(self) -> Instance:
|
||||
@@ -289,76 +269,6 @@ class GurobiSolver(InternalSolver):
|
||||
slacks=slacks,
|
||||
)
|
||||
|
||||
@overrides
|
||||
def get_constraints_old(self, with_static: bool = True) -> Dict[str, Constraint]:
|
||||
model = self.model
|
||||
assert model is not None
|
||||
self._raise_if_callback()
|
||||
if self._dirty:
|
||||
model.update()
|
||||
self._dirty = False
|
||||
|
||||
gp_constrs = model.getConstrs()
|
||||
constr_names = model.getAttr("constrName", gp_constrs)
|
||||
lhs: Optional[List[Dict]] = None
|
||||
rhs = None
|
||||
sense = None
|
||||
dual_value = None
|
||||
sa_rhs_up = None
|
||||
sa_rhs_down = None
|
||||
slack = None
|
||||
basis_status = None
|
||||
|
||||
if with_static:
|
||||
var_names = model.getAttr("varName", model.getVars())
|
||||
rhs = model.getAttr("rhs", gp_constrs)
|
||||
sense = model.getAttr("sense", gp_constrs)
|
||||
lhs = []
|
||||
for (i, gp_constr) in enumerate(gp_constrs):
|
||||
expr = model.getRow(gp_constr)
|
||||
lhsi = {}
|
||||
for j in range(expr.size()):
|
||||
lhsi[var_names[expr.getVar(j).index]] = expr.getCoeff(j)
|
||||
lhs.append(lhsi)
|
||||
if self._has_lp_solution:
|
||||
dual_value = model.getAttr("pi", gp_constrs)
|
||||
sa_rhs_up = model.getAttr("saRhsUp", gp_constrs)
|
||||
sa_rhs_down = model.getAttr("saRhsLow", gp_constrs)
|
||||
basis_status = model.getAttr("cbasis", gp_constrs)
|
||||
if self._has_lp_solution or self._has_mip_solution:
|
||||
slack = model.getAttr("slack", gp_constrs)
|
||||
|
||||
constraints: Dict[str, Constraint] = {}
|
||||
for (i, gp_constr) in enumerate(gp_constrs):
|
||||
assert (
|
||||
constr_names[i] not in constraints
|
||||
), f"Duplicated constraint name detected: {constr_names[i]}"
|
||||
constraint = Constraint()
|
||||
if with_static:
|
||||
assert lhs is not None
|
||||
assert rhs is not None
|
||||
assert sense is not None
|
||||
constraint.lhs = lhs[i]
|
||||
constraint.rhs = rhs[i]
|
||||
constraint.sense = sense[i]
|
||||
if dual_value is not None:
|
||||
assert sa_rhs_up is not None
|
||||
assert sa_rhs_down is not None
|
||||
assert basis_status is not None
|
||||
constraint.dual_value = dual_value[i]
|
||||
constraint.sa_rhs_up = sa_rhs_up[i]
|
||||
constraint.sa_rhs_down = sa_rhs_down[i]
|
||||
if gp_constr.cbasis == 0:
|
||||
constraint.basis_status = "B"
|
||||
elif gp_constr.cbasis == -1:
|
||||
constraint.basis_status = "N"
|
||||
else:
|
||||
raise Exception(f"unknown cbasis: {gp_constr.cbasis}")
|
||||
if slack is not None:
|
||||
constraint.slack = slack[i]
|
||||
constraints[constr_names[i]] = constraint
|
||||
return constraints
|
||||
|
||||
@overrides
|
||||
def get_solution(self) -> Optional[Solution]:
|
||||
assert self.model is not None
|
||||
@@ -470,42 +380,6 @@ class GurobiSolver(InternalSolver):
|
||||
values=values,
|
||||
)
|
||||
|
||||
def is_constraint_satisfied(
|
||||
self,
|
||||
names: List[str],
|
||||
tol: float = 1e-6,
|
||||
) -> List[bool]:
|
||||
def _check(c: Tuple) -> bool:
|
||||
lhs, sense, rhs = c
|
||||
lhs_value = lhs.getValue()
|
||||
if sense == "=":
|
||||
return abs(lhs_value - rhs) < tol
|
||||
elif sense == ">":
|
||||
return lhs_value > rhs - tol
|
||||
else:
|
||||
return lhs_value < rhs - tol
|
||||
|
||||
constrs = [self._relaxed_constrs[n] for n in names]
|
||||
return list(map(_check, constrs))
|
||||
|
||||
@overrides
|
||||
def is_constraint_satisfied_old(
|
||||
self,
|
||||
constr: Constraint,
|
||||
tol: float = 1e-6,
|
||||
) -> bool:
|
||||
assert constr.lhs is not None
|
||||
lhs = 0.0
|
||||
for (varname, coeff) in constr.lhs.items():
|
||||
var = self._varname_to_var[varname]
|
||||
lhs += self._get_value(var) * coeff
|
||||
if constr.sense == "<":
|
||||
return lhs <= constr.rhs + tol
|
||||
elif constr.sense == ">":
|
||||
return lhs >= constr.rhs - tol
|
||||
else:
|
||||
return abs(constr.rhs - lhs) < abs(tol)
|
||||
|
||||
@overrides
|
||||
def is_infeasible(self) -> bool:
|
||||
assert self.model is not None
|
||||
@@ -521,13 +395,7 @@ class GurobiSolver(InternalSolver):
|
||||
self.model.update()
|
||||
|
||||
@overrides
|
||||
def remove_constraint(self, name: str) -> None:
|
||||
assert self.model is not None
|
||||
constr = self.model.getConstrByName(name)
|
||||
self.model.remove(constr)
|
||||
|
||||
@overrides
|
||||
def remove_constraints(self, names: List[str]) -> None:
|
||||
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||
assert self.model is not None
|
||||
constrs = [self.model.getConstrByName(n) for n in names]
|
||||
self.model.remove(constrs)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from miplearn.features import Constraint, VariableFeatures, ConstraintFeatures
|
||||
from miplearn.instance.base import Instance
|
||||
@@ -51,21 +51,185 @@ class InternalSolver(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def solve_lp(
|
||||
self,
|
||||
tee: bool = False,
|
||||
) -> LPSolveStats:
|
||||
"""
|
||||
Solves the LP relaxation of the currently loaded instance. After this
|
||||
method finishes, the solution can be retrieved by calling `get_solution`.
|
||||
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
||||
"""Adds the given constraints to the model."""
|
||||
pass
|
||||
|
||||
This method should not permanently modify the problem. That is, subsequent
|
||||
calls to `solve` should solve the original MIP, not the LP relaxation.
|
||||
@abstractmethod
|
||||
def are_constraints_satisfied(
|
||||
self,
|
||||
cf: ConstraintFeatures,
|
||||
tol: float = 1e-5,
|
||||
) -> Tuple[bool, ...]:
|
||||
"""
|
||||
Checks whether the current solution satisfies the given constraints.
|
||||
"""
|
||||
pass
|
||||
|
||||
def are_callbacks_supported(self) -> bool:
|
||||
"""
|
||||
Returns True if this solver supports native callbacks, such as lazy constraints
|
||||
callback or user cuts callback.
|
||||
"""
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def build_test_instance_infeasible(self) -> Instance:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_test_instance_knapsack(self) -> Instance:
|
||||
"""
|
||||
Returns an instance corresponding to the following MIP, for testing purposes:
|
||||
|
||||
maximize 505 x0 + 352 x1 + 458 x2 + 220 x3
|
||||
s.t. eq_capacity: z = 23 x0 + 26 x1 + 20 x2 + 18 x3
|
||||
x0, x1, x2, x3 binary
|
||||
0 <= z <= 67 continuous
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_test_instance_redundancy(self) -> Instance:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def clone(self) -> "InternalSolver":
|
||||
"""
|
||||
Returns a new copy of this solver with identical parameters, but otherwise
|
||||
completely unitialized.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fix(self, solution: Solution) -> None:
|
||||
"""
|
||||
Fixes the values of a subset of decision variables. Missing values in the
|
||||
solution indicate variables that should be left free.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_solution(self) -> Optional[Solution]:
|
||||
"""
|
||||
Returns current solution found by the solver.
|
||||
|
||||
If called after `solve`, returns the best primal solution found during
|
||||
the search. If called after `solve_lp`, returns the optimal solution
|
||||
to the LP relaxation. If no primal solution is available, return None.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_constraint_attrs(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of constraint attributes supported by this solver. Used for
|
||||
testing purposes only.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_constraints(
|
||||
self,
|
||||
with_static: bool = True,
|
||||
with_sa: bool = True,
|
||||
with_lhs: bool = True,
|
||||
) -> ConstraintFeatures:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_variable_attrs(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of variable attributes supported by this solver. Used for
|
||||
testing purposes only.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_variables(
|
||||
self,
|
||||
with_static: bool = True,
|
||||
with_sa: bool = True,
|
||||
) -> VariableFeatures:
|
||||
"""
|
||||
Returns a description of the decision variables in the problem.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tee
|
||||
If true, prints the solver log to the screen.
|
||||
with_static: bool
|
||||
If True, include features that do not change during the solution process,
|
||||
such as variable types and names. This parameter is used to reduce the
|
||||
amount of duplicated data collected by LearningSolver. Features that do
|
||||
not change are only collected once.
|
||||
with_sa: bool
|
||||
If True, collect sensitivity analysis information. For large models,
|
||||
collecting this information may be expensive, so this parameter is useful
|
||||
for reducing running times.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_infeasible(self) -> bool:
|
||||
"""
|
||||
Returns True if the model has been proved to be infeasible.
|
||||
Must be called after solve.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||
"""
|
||||
Removes the given constraints from the model.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def relax(self) -> None:
|
||||
"""
|
||||
Drops all integrality constraints from the model.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_branching_priorities(self, priorities: BranchPriorities) -> None:
|
||||
"""
|
||||
Sets the branching priorities for the given decision variables.
|
||||
|
||||
When the MIP solver needs to decide on which variable to branch, variables
|
||||
with higher priority are picked first, given that they are fractional.
|
||||
Ties are solved arbitrarily. By default, all variables have priority zero.
|
||||
|
||||
Missing values indicate variables whose priorities should not be modified.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def set_instance(
|
||||
self,
|
||||
instance: Instance,
|
||||
model: Any = None,
|
||||
) -> None:
|
||||
"""
|
||||
Loads the given instance into the solver.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
instance: Instance
|
||||
The instance to be loaded.
|
||||
model: Any
|
||||
The concrete optimization model corresponding to this instance
|
||||
(e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided,
|
||||
it will be generated by calling `instance.to_model()`.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_warm_start(self, solution: Solution) -> None:
|
||||
"""
|
||||
Sets the warm start to be used by the solver.
|
||||
|
||||
Only one warm start is supported. Calling this function when a warm start
|
||||
already exists will remove the previous warm start.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -106,211 +270,20 @@ class InternalSolver(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_solution(self) -> Optional[Solution]:
|
||||
"""
|
||||
Returns current solution found by the solver.
|
||||
|
||||
If called after `solve`, returns the best primal solution found during
|
||||
the search. If called after `solve_lp`, returns the optimal solution
|
||||
to the LP relaxation. If no primal solution is available, return None.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_warm_start(self, solution: Solution) -> None:
|
||||
"""
|
||||
Sets the warm start to be used by the solver.
|
||||
|
||||
Only one warm start is supported. Calling this function when a warm start
|
||||
already exists will remove the previous warm start.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_instance(
|
||||
def solve_lp(
|
||||
self,
|
||||
instance: Instance,
|
||||
model: Any = None,
|
||||
) -> None:
|
||||
tee: bool = False,
|
||||
) -> LPSolveStats:
|
||||
"""
|
||||
Loads the given instance into the solver.
|
||||
Solves the LP relaxation of the currently loaded instance. After this
|
||||
method finishes, the solution can be retrieved by calling `get_solution`.
|
||||
|
||||
This method should not permanently modify the problem. That is, subsequent
|
||||
calls to `solve` should solve the original MIP, not the LP relaxation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
instance: Instance
|
||||
The instance to be loaded.
|
||||
model: Any
|
||||
The concrete optimization model corresponding to this instance
|
||||
(e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided,
|
||||
it will be generated by calling `instance.to_model()`.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fix(self, solution: Solution) -> None:
|
||||
"""
|
||||
Fixes the values of a subset of decision variables. Missing values in the
|
||||
solution indicate variables that should be left free.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_branching_priorities(self, priorities: BranchPriorities) -> None:
|
||||
"""
|
||||
Sets the branching priorities for the given decision variables.
|
||||
|
||||
When the MIP solver needs to decide on which variable to branch, variables
|
||||
with higher priority are picked first, given that they are fractional.
|
||||
Ties are solved arbitrarily. By default, all variables have priority zero.
|
||||
|
||||
Missing values indicate variables whose priorities should not be modified.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_constraints(
|
||||
self,
|
||||
with_static: bool = True,
|
||||
with_sa: bool = True,
|
||||
with_lhs: bool = True,
|
||||
) -> ConstraintFeatures:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_constraints_old(self, with_static: bool = True) -> Dict[str, Constraint]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def add_constraint(self, constr: Constraint, name: str) -> None:
|
||||
"""
|
||||
Adds a given constraint to the model.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def add_constraints(self, cf: ConstraintFeatures) -> None:
|
||||
"""Adds the given constraints to the model."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def are_constraints_satisfied(
|
||||
self,
|
||||
cf: ConstraintFeatures,
|
||||
tol: float = 1e-5,
|
||||
) -> List[bool]:
|
||||
"""
|
||||
Checks whether the current solution satisfies the given constraints.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_constraint(self, name: str) -> None:
|
||||
"""
|
||||
Removes the constraint that has a given name from the model.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_constraints(self, names: List[str]) -> None:
|
||||
"""
|
||||
Removes the given constraints from the model.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_constraint_satisfied_old(
|
||||
self, constr: Constraint, tol: float = 1e-6
|
||||
) -> bool:
|
||||
"""
|
||||
Returns True if the current solution satisfies the given constraint.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def relax(self) -> None:
|
||||
"""
|
||||
Drops all integrality constraints from the model.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_infeasible(self) -> bool:
|
||||
"""
|
||||
Returns True if the model has been proved to be infeasible.
|
||||
Must be called after solve.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def clone(self) -> "InternalSolver":
|
||||
"""
|
||||
Returns a new copy of this solver with identical parameters, but otherwise
|
||||
completely unitialized.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_test_instance_infeasible(self) -> Instance:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_test_instance_redundancy(self) -> Instance:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_test_instance_knapsack(self) -> Instance:
|
||||
"""
|
||||
Returns an instance corresponding to the following MIP, for testing purposes:
|
||||
|
||||
maximize 505 x0 + 352 x1 + 458 x2 + 220 x3
|
||||
s.t. eq_capacity: z = 23 x0 + 26 x1 + 20 x2 + 18 x3
|
||||
x0, x1, x2, x3 binary
|
||||
0 <= z <= 67 continuous
|
||||
"""
|
||||
pass
|
||||
|
||||
def are_callbacks_supported(self) -> bool:
|
||||
"""
|
||||
Returns True if this solver supports native callbacks, such as lazy constraints
|
||||
callback or user cuts callback.
|
||||
"""
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def get_variables(
|
||||
self,
|
||||
with_static: bool = True,
|
||||
with_sa: bool = True,
|
||||
) -> VariableFeatures:
|
||||
"""
|
||||
Returns a description of the decision variables in the problem.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
with_static: bool
|
||||
If True, include features that do not change during the solution process,
|
||||
such as variable types and names. This parameter is used to reduce the
|
||||
amount of duplicated data collected by LearningSolver. Features that do
|
||||
not change are only collected once.
|
||||
with_sa: bool
|
||||
If True, collect sensitivity analysis information. For large models,
|
||||
collecting this information may be expensive, so this parameter is useful
|
||||
for reducing running times.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_constraint_attrs(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of constraint attributes supported by this solver. Used for
|
||||
testing purposes only.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_variable_attrs(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of variable attributes supported by this solver. Used for
|
||||
testing purposes only.
|
||||
tee
|
||||
If true, prints the solver log to the screen.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -28,7 +28,6 @@ from miplearn.solvers.internal import (
|
||||
IterationCallback,
|
||||
LazyCallback,
|
||||
MIPSolveStats,
|
||||
Constraint,
|
||||
)
|
||||
from miplearn.types import (
|
||||
SolverParams,
|
||||
@@ -69,31 +68,12 @@ class BasePyomoSolver(InternalSolver):
|
||||
for (key, value) in params.items():
|
||||
self._pyomo_solver.options[key] = value
|
||||
|
||||
@overrides
|
||||
def add_constraint(
|
||||
self,
|
||||
constr: Any,
|
||||
name: str,
|
||||
) -> None:
|
||||
assert self.model is not None
|
||||
if isinstance(constr, Constraint):
|
||||
assert constr.lhs is not None
|
||||
lhs = 0.0
|
||||
for (varname, coeff) in constr.lhs.items():
|
||||
var = self._varname_to_var[varname]
|
||||
lhs += var * coeff
|
||||
if constr.sense == "=":
|
||||
expr = lhs == constr.rhs
|
||||
elif constr.sense == "<":
|
||||
expr = lhs <= constr.rhs
|
||||
else:
|
||||
expr = lhs >= constr.rhs
|
||||
cl = pe.Constraint(expr=expr, name=name)
|
||||
self.model.add_component(name, cl)
|
||||
self._pyomo_solver.add_constraint(cl)
|
||||
self._cname_to_constr[name] = cl
|
||||
else:
|
||||
self._pyomo_solver.add_constraint(constr)
|
||||
self._pyomo_solver.add_constraint(constr)
|
||||
self._termination_condition = ""
|
||||
self._has_lp_solution = False
|
||||
self._has_mip_solution = False
|
||||
@@ -133,7 +113,7 @@ class BasePyomoSolver(InternalSolver):
|
||||
self,
|
||||
cf: ConstraintFeatures,
|
||||
tol: float = 1e-5,
|
||||
) -> List[bool]:
|
||||
) -> Tuple[bool, ...]:
|
||||
assert cf.names is not None
|
||||
assert cf.lhs is not None
|
||||
assert cf.rhs is not None
|
||||
@@ -150,7 +130,7 @@ class BasePyomoSolver(InternalSolver):
|
||||
result.append(lhs >= cf.rhs[i] - tol)
|
||||
else:
|
||||
result.append(abs(cf.rhs[i] - lhs) < tol)
|
||||
return result
|
||||
return tuple(result)
|
||||
|
||||
@overrides
|
||||
def build_test_instance_infeasible(self) -> Instance:
|
||||
@@ -277,30 +257,6 @@ class BasePyomoSolver(InternalSolver):
|
||||
dual_values=dual_values_t,
|
||||
)
|
||||
|
||||
@overrides
|
||||
def get_constraints_old(self, with_static: bool = True) -> Dict[str, Constraint]:
|
||||
assert self.model is not None
|
||||
|
||||
constraints = {}
|
||||
for constr in self.model.component_objects(pyomo.core.Constraint):
|
||||
if isinstance(constr, pe.ConstraintList):
|
||||
for idx in constr:
|
||||
name = f"{constr.name}[{idx}]"
|
||||
assert name not in constraints
|
||||
constraints[name] = self._parse_pyomo_constraint(
|
||||
constr[idx],
|
||||
with_static=with_static,
|
||||
)
|
||||
else:
|
||||
name = constr.name
|
||||
assert name not in constraints
|
||||
constraints[name] = self._parse_pyomo_constraint(
|
||||
constr,
|
||||
with_static=with_static,
|
||||
)
|
||||
|
||||
return constraints
|
||||
|
||||
@overrides
|
||||
def get_constraint_attrs(self) -> List[str]:
|
||||
return [
|
||||
@@ -435,36 +391,12 @@ class BasePyomoSolver(InternalSolver):
|
||||
"values",
|
||||
]
|
||||
|
||||
@overrides
|
||||
def is_constraint_satisfied_old(
|
||||
self, constr: Constraint, tol: float = 1e-6
|
||||
) -> bool:
|
||||
lhs = 0.0
|
||||
assert constr.lhs is not None
|
||||
for (varname, coeff) in constr.lhs.items():
|
||||
var = self._varname_to_var[varname]
|
||||
lhs += var.value * coeff
|
||||
if constr.sense == "<":
|
||||
return lhs <= constr.rhs + tol
|
||||
elif constr.sense == ">":
|
||||
return lhs >= constr.rhs - tol
|
||||
else:
|
||||
return abs(constr.rhs - lhs) < abs(tol)
|
||||
|
||||
@overrides
|
||||
def is_infeasible(self) -> bool:
|
||||
return self._termination_condition == TerminationCondition.infeasible
|
||||
|
||||
@overrides
|
||||
def remove_constraint(self, name: str) -> None:
|
||||
assert self.model is not None
|
||||
constr = self._cname_to_constr[name]
|
||||
del self._cname_to_constr[name]
|
||||
self.model.del_component(constr)
|
||||
self._pyomo_solver.remove_constraint(constr)
|
||||
|
||||
@overrides
|
||||
def remove_constraints(self, names: List[str]) -> None:
|
||||
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||
assert self.model is not None
|
||||
for name in names:
|
||||
constr = self._cname_to_constr[name]
|
||||
@@ -627,46 +559,6 @@ class BasePyomoSolver(InternalSolver):
|
||||
def _get_warm_start_regexp(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
def _parse_pyomo_constraint(
|
||||
self,
|
||||
pyomo_constr: pyomo.core.Constraint,
|
||||
with_static: bool = True,
|
||||
) -> Constraint:
|
||||
assert self.model is not None
|
||||
constr = Constraint()
|
||||
|
||||
if with_static:
|
||||
# Extract RHS and sense
|
||||
has_ub = pyomo_constr.has_ub()
|
||||
has_lb = pyomo_constr.has_lb()
|
||||
assert (
|
||||
(not has_lb)
|
||||
or (not has_ub)
|
||||
or pyomo_constr.upper() == pyomo_constr.lower()
|
||||
), "range constraints not supported"
|
||||
if not has_ub:
|
||||
constr.sense = ">"
|
||||
constr.rhs = pyomo_constr.lower()
|
||||
elif not has_lb:
|
||||
constr.sense = "<"
|
||||
constr.rhs = pyomo_constr.upper()
|
||||
else:
|
||||
constr.sense = "="
|
||||
constr.rhs = pyomo_constr.upper()
|
||||
|
||||
# Extract LHS
|
||||
constr.lhs = self._parse_pyomo_expr(pyomo_constr.body)
|
||||
|
||||
# Extract solution attributes
|
||||
if self._has_lp_solution:
|
||||
constr.dual_value = self.model.dual[pyomo_constr]
|
||||
|
||||
if self._has_mip_solution or self._has_lp_solution:
|
||||
constr.slack = self.model.slack[pyomo_constr]
|
||||
|
||||
# Build constraint
|
||||
return constr
|
||||
|
||||
def _parse_pyomo_expr(self, expr: Any) -> Dict[str, float]:
|
||||
lhs = {}
|
||||
if isinstance(expr, SumExpression):
|
||||
|
||||
@@ -200,7 +200,7 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
||||
rhs=(0.0,),
|
||||
senses=("<",),
|
||||
)
|
||||
assert_equals(solver.are_constraints_satisfied(cf), [False])
|
||||
assert_equals(solver.are_constraints_satisfied(cf), (False,))
|
||||
|
||||
# Add constraint and verify it affects solution
|
||||
solver.add_constraints(cf)
|
||||
@@ -227,10 +227,10 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
||||
)
|
||||
stats = solver.solve()
|
||||
assert_equals(stats.mip_lower_bound, 1030.0)
|
||||
assert_equals(solver.are_constraints_satisfied(cf), [True])
|
||||
assert_equals(solver.are_constraints_satisfied(cf), (True,))
|
||||
|
||||
# Remove the new constraint
|
||||
solver.remove_constraints(["cut"])
|
||||
solver.remove_constraints(("cut",))
|
||||
|
||||
# New constraint should no longer affect solution
|
||||
stats = solver.solve()
|
||||
|
||||
Reference in New Issue
Block a user