Implement bulk constraint methods

master
Alinson S. Xavier 4 years ago
parent 8e61b7be5f
commit 83c46d70a3
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -111,10 +111,59 @@ class GurobiSolver(InternalSolver):
self._has_lp_solution = False
self._has_mip_solution = False
@overrides
def add_constraints(self, cf: ConstraintFeatures) -> None:
assert cf.names is not None
assert cf.senses is not None
assert cf.lhs is not None
assert cf.rhs is not None
assert self.model is not None
for i in range(len(cf.names)):
sense = cf.senses[i]
lhs = self.gp.quicksum(
self._varname_to_var[varname] * coeff for (varname, coeff) in cf.lhs[i]
)
if sense == "=":
self.model.addConstr(lhs == cf.rhs[i], name=cf.names[i])
elif sense == "<":
self.model.addConstr(lhs <= cf.rhs[i], name=cf.names[i])
else:
self.model.addConstr(lhs >= cf.rhs[i], name=cf.names[i])
self.model.update()
self._dirty = True
self._has_lp_solution = False
self._has_mip_solution = False
@overrides
def are_callbacks_supported(self) -> bool:
return True
@overrides
def are_constraints_satisfied(
self,
cf: ConstraintFeatures,
tol: float = 1e-5,
) -> List[bool]:
assert cf.names is not None
assert cf.senses is not None
assert cf.lhs is not None
assert cf.rhs is not None
assert self.model is not None
result = []
for i in range(len(cf.names)):
sense = cf.senses[i]
lhs = sum(
self._varname_to_var[varname].x * coeff
for (varname, coeff) in cf.lhs[i]
)
if sense == "<":
result.append(lhs <= cf.rhs[i] + tol)
elif sense == ">":
result.append(lhs >= cf.rhs[i] - tol)
else:
result.append(abs(cf.rhs[i] - lhs) <= tol)
return result
@overrides
def build_test_instance_infeasible(self) -> Instance:
return GurobiTestInstanceInfeasible()
@ -477,6 +526,13 @@ class GurobiSolver(InternalSolver):
constr = self.model.getConstrByName(name)
self.model.remove(constr)
@overrides
def remove_constraints(self, names: List[str]) -> None:
assert self.model is not None
constrs = [self.model.getConstrByName(n) for n in names]
self.model.remove(constrs)
self.model.update()
@overrides
def set_instance(
self,

@ -186,6 +186,22 @@ class InternalSolver(ABC):
"""
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:
"""
@ -193,6 +209,13 @@ class InternalSolver(ABC):
"""
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
@ -235,6 +258,14 @@ class InternalSolver(ABC):
@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:
@ -250,12 +281,28 @@ class InternalSolver(ABC):
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.
Returns a list of constraint attributes supported by this solver. Used for
testing purposes only.
"""
pass
@ -263,6 +310,7 @@ class InternalSolver(ABC):
@abstractmethod
def get_variable_attrs(self) -> List[str]:
"""
Returns a list of variable attributes supported by this solver.
Returns a list of variable attributes supported by this solver. Used for
testing purposes only.
"""
pass

@ -98,10 +98,60 @@ class BasePyomoSolver(InternalSolver):
self._has_lp_solution = False
self._has_mip_solution = False
@overrides
def add_constraints(self, cf: ConstraintFeatures) -> None:
assert cf.names is not None
assert cf.senses is not None
assert cf.lhs is not None
assert cf.rhs is not None
assert self.model is not None
for (i, name) in enumerate(cf.names):
lhs = 0.0
for (varname, coeff) in cf.lhs[i]:
var = self._varname_to_var[varname]
lhs += var * coeff
if cf.senses[i] == "=":
expr = lhs == cf.rhs[i]
elif cf.senses[i] == "<":
expr = lhs <= cf.rhs[i]
else:
expr = lhs >= cf.rhs[i]
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
self._termination_condition = ""
self._has_lp_solution = False
self._has_mip_solution = False
@overrides
def are_callbacks_supported(self) -> bool:
return False
@overrides
def are_constraints_satisfied(
self,
cf: ConstraintFeatures,
tol: float = 1e-5,
) -> List[bool]:
assert cf.names is not None
assert cf.lhs is not None
assert cf.rhs is not None
assert cf.senses is not None
result = []
for (i, name) in enumerate(cf.names):
lhs = 0.0
for (varname, coeff) in cf.lhs[i]:
var = self._varname_to_var[varname]
lhs += var.value * coeff
if cf.senses[i] == "<":
result.append(lhs <= cf.rhs[i] + tol)
elif cf.senses[i] == ">":
result.append(lhs >= cf.rhs[i] - tol)
else:
result.append(abs(cf.rhs[i] - lhs) < tol)
return result
@overrides
def build_test_instance_infeasible(self) -> Instance:
return PyomoTestInstanceInfeasible()
@ -413,6 +463,15 @@ class BasePyomoSolver(InternalSolver):
self.model.del_component(constr)
self._pyomo_solver.remove_constraint(constr)
@overrides
def remove_constraints(self, names: List[str]) -> None:
assert self.model is not None
for name in names:
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 set_instance(
self,

@ -183,42 +183,54 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
# Fetch constraints (after-mip)
assert_equals(
_round_constraints(solver.get_constraints_old(with_static=False)),
{"eq_capacity": Constraint(slack=0.0)},
_round(solver.get_constraints(with_static=False)),
_filter_attrs(
solver.get_constraint_attrs(),
ConstraintFeatures(
names=("eq_capacity",),
slacks=(0.0,),
),
),
)
# Build a new constraint
cut = Constraint(lhs={"x[0]": 1.0}, sense="<", rhs=0.0)
assert not solver.is_constraint_satisfied_old(cut)
# Build new constraint and verify that it is violated
cf = ConstraintFeatures(
names=("cut",),
lhs=((("x[0]", 1.0),),),
rhs=(0.0,),
senses=("<",),
)
assert_equals(solver.are_constraints_satisfied(cf), [False])
# Add new constraint and verify that it is listed. Modifying the model should
# also clear the current solution.
solver.add_constraint(cut, "cut")
# Add constraint and verify it affects solution
solver.add_constraints(cf)
assert_equals(
_round_constraints(solver.get_constraints_old()),
{
"eq_capacity": Constraint(
lazy=False,
lhs={"x[0]": 23.0, "x[1]": 26.0, "x[2]": 20.0, "x[3]": 18.0, "z": -1.0},
rhs=0.0,
sense="=",
_round(solver.get_constraints(with_static=True)),
_filter_attrs(
solver.get_constraint_attrs(),
ConstraintFeatures(
names=("eq_capacity", "cut"),
rhs=(0.0, 0.0),
lhs=(
(
("x[0]", 23.0),
("x[1]", 26.0),
("x[2]", 20.0),
("x[3]", 18.0),
("z", -1.0),
),
(("x[0]", 1.0),),
),
senses=("=", "<"),
),
"cut": Constraint(
lazy=False,
lhs={"x[0]": 1.0},
rhs=0.0,
sense="<",
),
},
)
# Re-solve MIP and verify that constraint affects the solution
stats = solver.solve()
assert_equals(stats.mip_lower_bound, 1030.0)
assert solver.is_constraint_satisfied_old(cut)
assert_equals(solver.are_constraints_satisfied(cf), [True])
# Remove the new constraint
solver.remove_constraint("cut")
solver.remove_constraints(["cut"])
# New constraint should no longer affect solution
stats = solver.solve()

Loading…
Cancel
Save