Only include static features in after-load

master
Alinson S. Xavier 5 years ago
parent 8f41278713
commit ef7a50e871
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -62,9 +62,9 @@ class DynamicConstraintsComponent(Component):
# Features
features = []
assert sample.after_lp is not None
assert sample.after_lp.instance is not None
features.extend(sample.after_lp.instance.to_list())
assert sample.after_load is not None
assert sample.after_load.instance is not None
features.extend(sample.after_load.instance.to_list())
features.extend(instance.get_constraint_features(cid))
for ci in features:
assert isinstance(ci, float), (

@ -149,6 +149,7 @@ class PrimalSolutionComponent(Component):
x: Dict = {}
y: Dict = {}
assert sample.after_load is not None
assert sample.after_load.instance is not None
assert sample.after_load.variables is not None
for (var_name, var) in sample.after_load.variables.items():
# Initialize categories
@ -160,14 +161,11 @@ class PrimalSolutionComponent(Component):
y[category] = []
# Features
sf = sample.after_load
features = list(sample.after_load.instance.to_list())
features.extend(sample.after_load.variables[var_name].to_list())
if sample.after_lp is not None:
sf = sample.after_lp
assert sf.instance is not None
features = list(sf.instance.to_list())
assert sf.variables is not None
assert sf.variables[var_name] is not None
features.extend(sf.variables[var_name].to_list())
assert sample.after_lp.variables is not None
features.extend(sample.after_lp.variables[var_name].to_list())
x[category].append(features)
# Labels

@ -82,7 +82,7 @@ class Constraint:
category: Optional[Hashable] = None
dual_value: Optional[float] = None
lazy: bool = False
lhs: Dict[str, float] = lambda: {} # type: ignore
lhs: Optional[Dict[str, float]] = None
rhs: float = 0.0
sa_rhs_down: Optional[float] = None
sa_rhs_up: Optional[float] = None
@ -136,10 +136,19 @@ class FeaturesExtractor:
) -> None:
self.solver = internal_solver
def extract(self, instance: "Instance") -> Features:
def extract(
self,
instance: "Instance",
with_static: bool = True,
) -> Features:
features = Features()
features.variables = self.solver.get_variables()
features.constraints = self.solver.get_constraints()
features.variables = self.solver.get_variables(
with_static=with_static,
)
features.constraints = self.solver.get_constraints(
with_static=with_static,
)
if with_static:
self._extract_user_features_vars(instance, features)
self._extract_user_features_constrs(instance, features)
self._extract_user_features_instance(instance, features)

@ -84,6 +84,7 @@ class GurobiSolver(InternalSolver):
@overrides
def add_constraint(self, constr: Constraint, name: str) -> None:
assert self.model 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()
@ -153,23 +154,36 @@ class GurobiSolver(InternalSolver):
]
@overrides
def get_constraints(self) -> Dict[str, Constraint]:
def get_constraints(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()
var_names = model.getAttr("varName", model.getVars())
constr_names = model.getAttr("constrName", gp_constrs)
rhs = model.getAttr("rhs", gp_constrs)
sense = model.getAttr("sense", 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)
@ -177,16 +191,20 @@ class GurobiSolver(InternalSolver):
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):
expr = model.getRow(gp_constr)
lhs = {}
for j in range(expr.size()):
lhs[var_names[expr.getVar(j).index]] = expr.getCoeff(j)
assert (
constr_names[i] not in constraints
), f"Duplicated constraint name detected: {constr_names[i]}"
constraint = Constraint(lhs=lhs, rhs=rhs[i], sense=sense[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
@ -247,13 +265,10 @@ class GurobiSolver(InternalSolver):
]
@overrides
def get_variables(self) -> Dict[str, Variable]:
def get_variables(self, with_static: bool = True) -> Dict[str, Variable]:
assert self.model is not None
variables = {}
gp_vars = self.model.getVars()
lb = self.model.getAttr("lb", gp_vars)
ub = self.model.getAttr("ub", gp_vars)
obj_coeff = self.model.getAttr("obj", gp_vars)
names = self.model.getAttr("varName", gp_vars)
values = None
rc = None
@ -264,6 +279,13 @@ class GurobiSolver(InternalSolver):
sa_lb_up = None
sa_lb_down = None
vbasis = None
ub = None
lb = None
obj_coeff = None
if with_static:
lb = self.model.getAttr("lb", gp_vars)
ub = self.model.getAttr("ub", gp_vars)
obj_coeff = self.model.getAttr("obj", gp_vars)
if self.model.solCount > 0:
values = self.model.getAttr("x", gp_vars)
if self._has_lp_solution:
@ -281,12 +303,15 @@ class GurobiSolver(InternalSolver):
assert (
names[i] not in variables
), f"Duplicated variable name detected: {names[i]}"
var = Variable(
lower_bound=lb[i],
upper_bound=ub[i],
obj_coeff=obj_coeff[i],
type=self._original_vtype[gp_var],
)
var = Variable()
if with_static:
assert lb is not None
assert ub is not None
assert obj_coeff is not None
var.lower_bound = lb[i]
var.upper_bound = ub[i]
var.obj_coeff = obj_coeff[i]
var.type = self._original_vtype[gp_var]
if values is not None:
var.value = values[i]
if rc is not None:
@ -319,6 +344,7 @@ class GurobiSolver(InternalSolver):
@overrides
def is_constraint_satisfied(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]

@ -170,7 +170,7 @@ class InternalSolver(ABC, EnforceOverrides):
raise NotImplementedError()
@abstractmethod
def get_constraints(self) -> Dict[str, Constraint]:
def get_constraints(self, with_static: bool = True) -> Dict[str, Constraint]:
pass
@abstractmethod
@ -237,7 +237,7 @@ class InternalSolver(ABC, EnforceOverrides):
return False
@abstractmethod
def get_variables(self) -> Dict[str, Variable]:
def get_variables(self, with_static: bool = True) -> Dict[str, Variable]:
pass
@abstractmethod

@ -187,7 +187,10 @@ class LearningSolver:
# Extract features (after-lp)
# -------------------------------------------------------
logger.info("Extracting features (after-lp)...")
features = FeaturesExtractor(self.internal_solver).extract(instance)
features = FeaturesExtractor(self.internal_solver).extract(
instance,
with_static=False,
)
features.extra = {}
features.lp_solve = lp_stats
sample.after_lp = features
@ -249,7 +252,10 @@ class LearningSolver:
# Extract features (after-mip)
# -------------------------------------------------------
logger.info("Extracting features (after-mip)...")
features = FeaturesExtractor(self.internal_solver).extract(instance)
features = FeaturesExtractor(self.internal_solver).extract(
instance,
with_static=False,
)
features.mip_solve = mip_stats
features.extra = {}
sample.after_mip = features

@ -77,6 +77,7 @@ class BasePyomoSolver(InternalSolver):
) -> 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]
@ -127,7 +128,7 @@ class BasePyomoSolver(InternalSolver):
self._pyomo_solver.update_var(var)
@overrides
def get_constraints(self) -> Dict[str, Constraint]:
def get_constraints(self, with_static: bool = True) -> Dict[str, Constraint]:
assert self.model is not None
constraints = {}
@ -136,11 +137,17 @@ class BasePyomoSolver(InternalSolver):
for idx in constr:
name = f"{constr.name}[{idx}]"
assert name not in constraints
constraints[name] = self._parse_pyomo_constraint(constr[idx])
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)
constraints[name] = self._parse_pyomo_constraint(
constr,
with_static=with_static,
)
return constraints
@ -169,7 +176,7 @@ class BasePyomoSolver(InternalSolver):
return solution
@overrides
def get_variables(self) -> Dict[str, Variable]:
def get_variables(self, with_static: bool = True) -> Dict[str, Variable]:
assert self.model is not None
variables = {}
for var in self.model.component_objects(pyomo.core.Var):
@ -177,7 +184,10 @@ class BasePyomoSolver(InternalSolver):
varname = f"{var}[{idx}]"
if idx is None:
varname = str(var)
variables[varname] = self._parse_pyomo_variable(var[idx])
variables[varname] = self._parse_pyomo_variable(
var[idx],
with_static=with_static,
)
return variables
@overrides
@ -201,6 +211,7 @@ class BasePyomoSolver(InternalSolver):
@overrides
def is_constraint_satisfied(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
@ -378,14 +389,20 @@ class BasePyomoSolver(InternalSolver):
def _get_warm_start_regexp(self) -> Optional[str]:
return None
def _parse_pyomo_variable(self, var: pyomo.core.Var) -> Variable:
def _parse_pyomo_variable(
self,
pyomo_var: pyomo.core.Var,
with_static: bool = True,
) -> Variable:
assert self.model is not None
variable = Variable()
if with_static:
# Variable type
vtype: Optional[str] = None
if var.domain == pyomo.core.Binary:
if pyomo_var.domain == pyomo.core.Binary:
vtype = "B"
elif var.domain in [
elif pyomo_var.domain in [
pyomo.core.Reals,
pyomo.core.NonNegativeReals,
pyomo.core.NonPositiveReals,
@ -394,42 +411,43 @@ class BasePyomoSolver(InternalSolver):
]:
vtype = "C"
if vtype is None:
raise Exception(f"unknown variable domain: {var.domain}")
raise Exception(f"unknown variable domain: {pyomo_var.domain}")
variable.type = vtype
# Bounds
lb, ub = var.bounds
# Reduced costs
rc = None
if var in self.model.rc:
rc = self.model.rc[var]
lb, ub = pyomo_var.bounds
variable.upper_bound = float(ub)
variable.lower_bound = float(lb)
# Objective coefficient
obj_coeff = 0.0
if var.name in self._obj:
obj_coeff = self._obj[var.name]
return Variable(
value=var.value,
type=vtype,
lower_bound=float(lb),
upper_bound=float(ub),
obj_coeff=obj_coeff,
reduced_cost=rc,
)
if pyomo_var.name in self._obj:
obj_coeff = self._obj[pyomo_var.name]
variable.obj_coeff = obj_coeff
# Reduced costs
if pyomo_var in self.model.rc:
variable.reduced_cost = self.model.rc[pyomo_var]
variable.value = pyomo_var.value
return variable
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()
(not has_lb)
or (not has_ub)
or pyomo_constr.upper() == pyomo_constr.lower()
), "range constraints not supported"
if not has_ub:
constr.sense = ">"

@ -55,37 +55,6 @@ class GurobiPyomoSolver(BasePyomoSolver):
gvar = self._pyomo_solver._pyomo_var_to_solver_var_map[var]
gvar.setAttr(GRB.Attr.BranchPriority, int(round(priority)))
@overrides
def get_variables(self) -> Dict[str, Variable]:
variables = super().get_variables()
if self._has_lp_solution:
for (varname, var) in variables.items():
pvar = self._varname_to_var[varname]
gvar = self._pyomo_solver._pyomo_var_to_solver_var_map[pvar]
GurobiSolver._parse_gurobi_var_lp(gvar, var)
return variables
@overrides
def get_variable_attrs(self) -> List[str]:
return [
"basis_status",
"category",
"lower_bound",
"obj_coeff",
"reduced_cost",
"sa_lb_down",
"sa_lb_up",
"sa_obj_down",
"sa_obj_up",
"sa_ub_down",
"sa_ub_up",
"type",
"upper_bound",
"user_features",
"value",
]
@overrides
def _extract_node_count(self, log: str) -> int:
return max(1, int(self._pyomo_solver._solver_model.getAttr("NodeCount")))

@ -279,63 +279,25 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
assert isinstance(mip_stats.mip_wallclock_time, float)
assert mip_stats.mip_wallclock_time > 0
# Fetch variables (after-load)
# Fetch variables (after-mip)
assert_equals(
_round_variables(solver.get_variables()),
_round_variables(solver.get_variables(with_static=False)),
_remove_unsupported_var_attrs(
solver,
{
"x[0]": Variable(
lower_bound=0.0,
obj_coeff=505.0,
type="B",
upper_bound=1.0,
value=1.0,
),
"x[1]": Variable(
lower_bound=0.0,
obj_coeff=352.0,
type="B",
upper_bound=1.0,
value=0.0,
),
"x[2]": Variable(
lower_bound=0.0,
obj_coeff=458.0,
type="B",
upper_bound=1.0,
value=1.0,
),
"x[3]": Variable(
lower_bound=0.0,
obj_coeff=220.0,
type="B",
upper_bound=1.0,
value=1.0,
),
"z": Variable(
lower_bound=0.0,
obj_coeff=0.0,
type="C",
upper_bound=67.0,
value=61.0,
),
"x[0]": Variable(value=1.0),
"x[1]": Variable(value=0.0),
"x[2]": Variable(value=1.0),
"x[3]": Variable(value=1.0),
"z": Variable(value=61.0),
},
),
)
# Fetch constraints (after-mip)
assert_equals(
_round_constraints(solver.get_constraints()),
{
"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="=",
slack=0.0,
)
},
_round_constraints(solver.get_constraints(with_static=False)),
{"eq_capacity": Constraint(slack=0.0)},
)
# Build a new constraint

@ -28,18 +28,18 @@ def training_instances() -> List[Instance]:
instances = [cast(Instance, Mock(spec=Instance)) for _ in range(2)]
instances[0].samples = [
Sample(
after_lp=Features(instance=InstanceFeatures()),
after_load=Features(instance=InstanceFeatures()),
after_mip=Features(extra={"lazy_enforced": {"c1", "c2"}}),
),
Sample(
after_lp=Features(instance=InstanceFeatures()),
after_load=Features(instance=InstanceFeatures()),
after_mip=Features(extra={"lazy_enforced": {"c2", "c3"}}),
),
]
instances[0].samples[0].after_lp.instance.to_list = Mock( # type: ignore
instances[0].samples[0].after_load.instance.to_list = Mock( # type: ignore
return_value=[5.0]
)
instances[0].samples[1].after_lp.instance.to_list = Mock( # type: ignore
instances[0].samples[1].after_load.instance.to_list = Mock( # type: ignore
return_value=[5.0]
)
instances[0].get_constraint_category = Mock( # type: ignore
@ -60,11 +60,11 @@ def training_instances() -> List[Instance]:
)
instances[1].samples = [
Sample(
after_lp=Features(instance=InstanceFeatures()),
after_load=Features(instance=InstanceFeatures()),
after_mip=Features(extra={"lazy_enforced": {"c3", "c4"}}),
)
]
instances[1].samples[0].after_lp.instance.to_list = Mock( # type: ignore
instances[1].samples[0].after_load.instance.to_list = Mock( # type: ignore
return_value=[8.0]
)
instances[1].get_constraint_category = Mock( # type: ignore

@ -27,6 +27,7 @@ from miplearn.solvers.tests import assert_equals
def sample() -> Sample:
sample = Sample(
after_load=Features(
instance=InstanceFeatures(),
variables={
"x[0]": Variable(category="default"),
"x[1]": Variable(category=None),
@ -35,7 +36,6 @@ def sample() -> Sample:
},
),
after_lp=Features(
instance=InstanceFeatures(),
variables={
"x[0]": Variable(),
"x[1]": Variable(),
@ -52,7 +52,7 @@ def sample() -> Sample:
}
),
)
sample.after_lp.instance.to_list = Mock(return_value=[5.0]) # type: ignore
sample.after_load.instance.to_list = Mock(return_value=[5.0]) # type: ignore
sample.after_lp.variables["x[0]"].to_list = Mock( # type: ignore
return_value=[0.0, 0.0]
)

Loading…
Cancel
Save