You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
118 lines
4.6 KiB
118 lines
4.6 KiB
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
|
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
|
# Released under the modified BSD license. See COPYING.md for more details.
|
|
|
|
from io import StringIO
|
|
from typing import Callable
|
|
|
|
import gurobipy as gp
|
|
import numpy as np
|
|
from gurobipy import GRB, LinExpr
|
|
|
|
from ..h5 import H5File
|
|
from ..io import _RedirectOutput
|
|
|
|
|
|
class LazyCollector:
|
|
def __init__(
|
|
self,
|
|
min_constrs: int = 100_000,
|
|
time_limit: float = 900,
|
|
) -> None:
|
|
self.min_constrs = min_constrs
|
|
self.time_limit = time_limit
|
|
|
|
def collect(
|
|
self, data_filename: str, build_model: Callable, tol: float = 1e-6
|
|
) -> None:
|
|
h5_filename = f"{data_filename}.h5"
|
|
with H5File(h5_filename, "r+") as h5:
|
|
streams = [StringIO()]
|
|
lazy = None
|
|
with _RedirectOutput(streams):
|
|
slacks = h5.get_array("mip_constr_slacks")
|
|
assert slacks is not None
|
|
|
|
# Check minimum problem size
|
|
if len(slacks) < self.min_constrs:
|
|
print("Problem is too small. Skipping.")
|
|
h5.put_array("mip_constr_lazy", np.zeros(len(slacks)))
|
|
return
|
|
|
|
# Load model
|
|
print("Loading model...")
|
|
model = build_model(data_filename)
|
|
model.params.LazyConstraints = True
|
|
model.params.timeLimit = self.time_limit
|
|
gp_constrs = np.array(model.getConstrs())
|
|
gp_vars = np.array(model.getVars())
|
|
|
|
# Load constraints
|
|
lhs = h5.get_sparse("static_constr_lhs")
|
|
rhs = h5.get_array("static_constr_rhs")
|
|
sense = h5.get_array("static_constr_sense")
|
|
assert lhs is not None
|
|
assert rhs is not None
|
|
assert sense is not None
|
|
lhs_csr = lhs.tocsr()
|
|
lhs_csc = lhs.tocsc()
|
|
constr_idx = np.array(range(len(rhs)))
|
|
lazy = np.zeros(len(rhs))
|
|
|
|
# Drop loose constraints
|
|
selected = (slacks > 0) & ((sense == b"<") | (sense == b">"))
|
|
loose_constrs = gp_constrs[selected]
|
|
print(
|
|
f"Removing {len(loose_constrs):,d} constraints (out of {len(rhs):,d})..."
|
|
)
|
|
model.remove(list(loose_constrs))
|
|
|
|
# Filter to constraints that were dropped
|
|
lhs_csr = lhs_csr[selected, :]
|
|
lhs_csc = lhs_csc[selected, :]
|
|
rhs = rhs[selected]
|
|
sense = sense[selected]
|
|
constr_idx = constr_idx[selected]
|
|
lazy[selected] = 1
|
|
|
|
# Load warm start
|
|
var_names = h5.get_array("static_var_names")
|
|
var_values = h5.get_array("mip_var_values")
|
|
assert var_values is not None
|
|
assert var_names is not None
|
|
for (var_idx, var_name) in enumerate(var_names):
|
|
var = model.getVarByName(var_name.decode())
|
|
var.start = var_values[var_idx]
|
|
|
|
print("Solving MIP with lazy constraints callback...")
|
|
|
|
def callback(model: gp.Model, where: int) -> None:
|
|
assert rhs is not None
|
|
assert lazy is not None
|
|
assert sense is not None
|
|
|
|
if where == GRB.Callback.MIPSOL:
|
|
x_val = np.array(model.cbGetSolution(model.getVars()))
|
|
slack = lhs_csc * x_val - rhs
|
|
slack[sense == b">"] *= -1
|
|
is_violated = slack > tol
|
|
|
|
for (j, rhs_j) in enumerate(rhs):
|
|
if is_violated[j]:
|
|
lazy[constr_idx[j]] = 0
|
|
expr = LinExpr(
|
|
lhs_csr[j, :].data, gp_vars[lhs_csr[j, :].indices]
|
|
)
|
|
if sense[j] == b"<":
|
|
model.cbLazy(expr <= rhs_j)
|
|
elif sense[j] == b">":
|
|
model.cbLazy(expr >= rhs_j)
|
|
else:
|
|
raise RuntimeError(f"Unknown sense: {sense[j]}")
|
|
|
|
model.optimize(callback)
|
|
print(f"Marking {lazy.sum():,.0f} constraints as lazy...")
|
|
|
|
h5.put_array("mip_constr_lazy", lazy)
|
|
h5.put_scalar("mip_constr_lazy_log", streams[0].getvalue())
|