Add more variable features

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

@ -16,7 +16,7 @@ from .components.static_lazy import StaticLazyConstraintsComponent
from .features import ( from .features import (
Features, Features,
TrainingSample, TrainingSample,
VariableFeatures, Variable,
InstanceFeatures, InstanceFeatures,
) )
from .instance.base import Instance from .instance.base import Instance

@ -36,9 +36,22 @@ class InstanceFeatures:
@dataclass @dataclass
class VariableFeatures: class Variable:
basis_status: Optional[str] = None
category: Optional[Hashable] = None category: Optional[Hashable] = None
lower_bound: Optional[float] = None
obj_coeff: Optional[float] = None
reduced_cost: Optional[float] = None
sa_lb_down: Optional[float] = None
sa_lb_up: Optional[float] = None
sa_obj_down: Optional[float] = None
sa_obj_up: Optional[float] = None
sa_ub_down: Optional[float] = None
sa_ub_up: Optional[float] = None
type: Optional[str] = None
upper_bound: Optional[float] = None
user_features: Optional[List[float]] = None user_features: Optional[List[float]] = None
value: Optional[float] = None
@dataclass @dataclass
@ -59,7 +72,7 @@ class Constraint:
@dataclass @dataclass
class Features: class Features:
instance: Optional[InstanceFeatures] = None instance: Optional[InstanceFeatures] = None
variables: Optional[Dict[str, VariableFeatures]] = None variables: Optional[Dict[str, Variable]] = None
constraints: Optional[Dict[str, Constraint]] = None constraints: Optional[Dict[str, Constraint]] = None
@ -78,8 +91,8 @@ class FeaturesExtractor:
def _extract_variables( def _extract_variables(
self, self,
instance: "Instance", instance: "Instance",
) -> Dict[VariableName, VariableFeatures]: ) -> Dict[VariableName, Variable]:
result: Dict[VariableName, VariableFeatures] = {} result: Dict[VariableName, Variable] = {}
for var_name in self.solver.get_variable_names(): for var_name in self.solver.get_variable_names():
user_features: Optional[List[float]] = None user_features: Optional[List[float]] = None
category: Category = instance.get_variable_category(var_name) category: Category = instance.get_variable_category(var_name)
@ -102,7 +115,7 @@ class FeaturesExtractor:
f"Found {type(v).__name__} instead " f"Found {type(v).__name__} instead "
f"for var={var_name}." f"for var={var_name}."
) )
result[var_name] = VariableFeatures( result[var_name] = Variable(
category=category, category=category,
user_features=user_features, user_features=user_features,
) )

@ -10,7 +10,7 @@ from typing import List, Any, Dict, Optional, Hashable
from overrides import overrides from overrides import overrides
from miplearn.features import Constraint from miplearn.features import Constraint, Variable
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.solvers import _RedirectOutput from miplearn.solvers import _RedirectOutput
from miplearn.solvers.internal import ( from miplearn.solvers.internal import (
@ -415,6 +415,49 @@ class GurobiSolver(InternalSolver):
capacity=67.0, capacity=67.0,
) )
@overrides
def get_variables(self) -> Dict[str, Variable]:
assert self.model is not None
variables = {}
for gp_var in self.model.getVars():
name = gp_var.varName
assert len(name) > 0, f"empty variable name detected"
assert name not in variables, f"duplicated variable name detected: {name}"
var = self._parse_gurobi_var(gp_var)
variables[name] = var
return variables
def _parse_gurobi_var(self, gp_var: Any) -> Variable:
assert self.model is not None
var = Variable()
var.lower_bound = gp_var.lb
var.upper_bound = gp_var.ub
var.obj_coeff = gp_var.obj
var.type = gp_var.vtype
if self._has_lp_solution:
var.reduced_cost = gp_var.rc
var.sa_obj_up = gp_var.saobjUp
var.sa_obj_down = gp_var.saobjLow
var.sa_ub_up = gp_var.saubUp
var.sa_ub_down = gp_var.saubLow
var.sa_lb_up = gp_var.salbUp
var.sa_lb_down = gp_var.salbLow
vbasis = gp_var.vbasis
if vbasis == 0:
var.basis_status = "B"
elif vbasis == -1:
var.basis_status = "L"
elif vbasis == -2:
var.basis_status = "U"
elif vbasis == -3:
var.basis_status = "S"
else:
raise Exception(f"unknown vbasis: {vbasis}")
if self._has_lp_solution or self._has_mip_solution:
var.value = gp_var.x
return var
@overrides @overrides
def get_constraints(self) -> Dict[str, Constraint]: def get_constraints(self) -> Dict[str, Constraint]:
assert self.model is not None assert self.model is not None
@ -443,11 +486,11 @@ class GurobiSolver(InternalSolver):
if self._has_lp_solution: if self._has_lp_solution:
constr.dual_value = gp_constr.pi constr.dual_value = gp_constr.pi
constr.sa_rhs_up = gp_constr.sarhsup constr.sa_rhs_up = gp_constr.sarhsup
constr.sa_rhs_low = gp_constr.sarhslow constr.sa_rhs_down = gp_constr.sarhslow
if gp_constr.cbasis == 0: if gp_constr.cbasis == 0:
constr.basis_status = "b" constr.basis_status = "B"
elif gp_constr.cbasis == -1: elif gp_constr.cbasis == -1:
constr.basis_status = "n" constr.basis_status = "N"
else: else:
raise Exception(f"unknown cbasis: {gp_constr.cbasis}") raise Exception(f"unknown cbasis: {gp_constr.cbasis}")
if self._has_lp_solution or self._has_mip_solution: if self._has_lp_solution or self._has_mip_solution:
@ -474,6 +517,26 @@ class GurobiSolver(InternalSolver):
"user_features", "user_features",
] ]
@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",
]
class GurobiTestInstanceInfeasible(Instance): class GurobiTestInstanceInfeasible(Instance):
@overrides @overrides

@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
from overrides import EnforceOverrides from overrides import EnforceOverrides
from miplearn.features import Constraint from miplearn.features import Constraint, Variable
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.types import ( from miplearn.types import (
LPSolveStats, LPSolveStats,
@ -247,9 +247,21 @@ class InternalSolver(ABC, EnforceOverrides):
""" """
return False return False
@abstractmethod
def get_variables(self) -> Dict[str, Variable]:
pass
@abstractmethod @abstractmethod
def get_constraint_attrs(self) -> List[str]: 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.
""" """
pass
@abstractmethod
def get_variable_attrs(self) -> List[str]:
"""
Returns a list of variable attributes supported by this solver.
"""
pass pass

@ -19,6 +19,7 @@ from pyomo.core.expr.numeric_expr import SumExpression, MonomialTermExpression
from pyomo.opt import TerminationCondition from pyomo.opt import TerminationCondition
from pyomo.opt.base.solvers import SolverFactory from pyomo.opt.base.solvers import SolverFactory
from miplearn.features import Variable
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.solvers import _RedirectOutput from miplearn.solvers import _RedirectOutput
from miplearn.solvers.internal import ( from miplearn.solvers.internal import (
@ -363,6 +364,19 @@ class BasePyomoSolver(InternalSolver):
capacity=67.0, capacity=67.0,
) )
@overrides
def get_variables(self) -> Dict[str, Variable]:
assert self.model is not None
variables = {}
for var in self.model.component_objects(pyomo.core.Var):
for idx in var:
varname = f"{var}[{idx}]"
variables[varname] = self._parse_pyomo_variable(var[idx])
return variables
def _parse_pyomo_variable(self, var: pyomo.core.Var) -> Variable:
return Variable()
@overrides @overrides
def get_constraints(self) -> Dict[str, Constraint]: def get_constraints(self) -> Dict[str, Constraint]:
assert self.model is not None assert self.model is not None
@ -385,6 +399,7 @@ class BasePyomoSolver(InternalSolver):
self, self,
pyomo_constr: pyomo.core.Constraint, pyomo_constr: pyomo.core.Constraint,
) -> Constraint: ) -> Constraint:
assert self.model is not None
constr = Constraint() constr = Constraint()
# Extract RHS and sense # Extract RHS and sense
@ -448,6 +463,26 @@ class BasePyomoSolver(InternalSolver):
"user_features", "user_features",
] ]
@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",
]
class PyomoTestInstanceInfeasible(Instance): class PyomoTestInstanceInfeasible(Instance):
@overrides @overrides

@ -4,9 +4,10 @@
from typing import Any, Dict from typing import Any, Dict
from miplearn.features import Constraint from miplearn.features import Constraint, Variable
from miplearn.solvers.internal import InternalSolver from miplearn.solvers.internal import InternalSolver
inf = float("inf")
# NOTE: # NOTE:
# This file is in the main source folder, so that it can be called from Julia. # This file is in the main source folder, so that it can be called from Julia.
@ -20,10 +21,30 @@ def _round_constraints(constraints: Dict[str, Constraint]) -> Dict[str, Constrai
return constraints return constraints
def _round_variables(vars: Dict[str, Variable]) -> Dict[str, Variable]:
for (cname, c) in vars.items():
for attr in [
"upper_bound",
"lower_bound",
"obj_coeff",
"value",
"reduced_cost",
"sa_obj_up",
"sa_obj_down",
"sa_ub_up",
"sa_ub_down",
"sa_lb_up",
"sa_lb_down",
]:
if getattr(c, attr) is not None:
setattr(c, attr, round(getattr(c, attr), 6))
return vars
def _remove_unsupported_constr_attrs( def _remove_unsupported_constr_attrs(
solver: InternalSolver, solver: InternalSolver,
constraints: Dict[str, Constraint], constraints: Dict[str, Constraint],
): ) -> Dict[str, Constraint]:
for (cname, c) in constraints.items(): for (cname, c) in constraints.items():
to_remove = [] to_remove = []
for k in c.__dict__.keys(): for k in c.__dict__.keys():
@ -34,6 +55,20 @@ def _remove_unsupported_constr_attrs(
return constraints return constraints
def _remove_unsupported_var_attrs(
solver: InternalSolver,
variables: Dict[str, Variable],
) -> Dict[str, Variable]:
for (cname, c) in variables.items():
to_remove = []
for k in c.__dict__.keys():
if k not in solver.get_variable_attrs():
to_remove.append(k)
for k in to_remove:
setattr(c, k, None)
return variables
def run_internal_solver_tests(solver: InternalSolver) -> None: def run_internal_solver_tests(solver: InternalSolver) -> None:
run_basic_usage_tests(solver.clone()) run_basic_usage_tests(solver.clone())
run_warm_start_tests(solver.clone()) run_warm_start_tests(solver.clone())
@ -51,8 +86,36 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
# Fetch variables (after-load) # Fetch variables (after-load)
assert_equals( assert_equals(
solver.get_variable_names(), _round_variables(solver.get_variables()),
["x[0]", "x[1]", "x[2]", "x[3]"], _remove_unsupported_var_attrs(
solver,
{
"x[0]": Variable(
lower_bound=0.0,
obj_coeff=505.0,
type="B",
upper_bound=1.0,
),
"x[1]": Variable(
lower_bound=0.0,
obj_coeff=352.0,
type="B",
upper_bound=1.0,
),
"x[2]": Variable(
lower_bound=0.0,
obj_coeff=458.0,
type="B",
upper_bound=1.0,
),
"x[3]": Variable(
lower_bound=0.0,
obj_coeff=220.0,
type="B",
upper_bound=1.0,
),
},
),
) )
# Fetch constraints (after-load) # Fetch constraints (after-load)
@ -75,17 +138,75 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
assert_equals(round(lp_stats["LP value"], 3), 1287.923) assert_equals(round(lp_stats["LP value"], 3), 1287.923)
assert len(lp_stats["LP log"]) > 100 assert len(lp_stats["LP log"]) > 100
# Fetch variables (after-lp) # Fetch variables (after-load)
solution = solver.get_solution() assert_equals(
assert solution is not None _round_variables(solver.get_variables()),
assert solution["x[0]"] is not None _remove_unsupported_var_attrs(
assert solution["x[1]"] is not None solver,
assert solution["x[2]"] is not None {
assert solution["x[3]"] is not None "x[0]": Variable(
assert_equals(round(solution["x[0]"], 3), 1.000) basis_status="U",
assert_equals(round(solution["x[1]"], 3), 0.923) lower_bound=0.0,
assert_equals(round(solution["x[2]"], 3), 1.000) obj_coeff=505.0,
assert_equals(round(solution["x[3]"], 3), 0.000) reduced_cost=193.615385,
sa_lb_down=-inf,
sa_lb_up=1.0,
sa_obj_down=311.384615,
sa_obj_up=inf,
sa_ub_down=0.913043,
sa_ub_up=2.043478,
type="C",
upper_bound=1.0,
value=1.0,
),
"x[1]": Variable(
basis_status="B",
lower_bound=0.0,
obj_coeff=352.0,
reduced_cost=0.0,
sa_lb_down=-inf,
sa_lb_up=0.923077,
sa_obj_down=317.777778,
sa_obj_up=570.869565,
sa_ub_down=0.923077,
sa_ub_up=inf,
type="C",
upper_bound=1.0,
value=0.923077,
),
"x[2]": Variable(
basis_status="U",
lower_bound=0.0,
obj_coeff=458.0,
reduced_cost=187.230769,
sa_lb_down=-inf,
sa_lb_up=1.0,
sa_obj_down=270.769231,
sa_obj_up=inf,
sa_ub_down=0.9,
sa_ub_up=2.2,
type="C",
upper_bound=1.0,
value=1.0,
),
"x[3]": Variable(
basis_status="L",
lower_bound=0.0,
obj_coeff=220.0,
reduced_cost=-23.692308,
sa_lb_down=-0.111111,
sa_lb_up=1.0,
sa_obj_down=-inf,
sa_obj_up=243.692308,
sa_ub_down=0.0,
sa_ub_up=inf,
type="C",
upper_bound=1.0,
value=0.0,
),
},
),
)
# Fetch constraints (after-lp) # Fetch constraints (after-lp)
assert_equals( assert_equals(
@ -100,9 +221,9 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
sense="<", sense="<",
slack=0.0, slack=0.0,
dual_value=13.538462, dual_value=13.538462,
sa_rhs_down=None, sa_rhs_down=43.0,
sa_rhs_up=69.0, sa_rhs_up=69.0,
basis_status="n", basis_status="N",
) )
}, },
), ),
@ -122,17 +243,43 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
assert_equals(mip_stats["Sense"], "max") assert_equals(mip_stats["Sense"], "max")
assert isinstance(mip_stats["Wallclock time"], float) assert isinstance(mip_stats["Wallclock time"], float)
# Fetch variables (after-mip) # Fetch variables (after-load)
solution = solver.get_solution() assert_equals(
assert solution is not None _round_variables(solver.get_variables()),
assert solution["x[0]"] is not None _remove_unsupported_var_attrs(
assert solution["x[1]"] is not None solver,
assert solution["x[2]"] is not None {
assert solution["x[3]"] is not None "x[0]": Variable(
assert_equals(solution["x[0]"], 1.0) lower_bound=0.0,
assert_equals(solution["x[1]"], 0.0) obj_coeff=505.0,
assert_equals(solution["x[2]"], 1.0) type="B",
assert_equals(solution["x[3]"], 1.0) 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,
),
},
),
)
# Fetch constraints (after-mip) # Fetch constraints (after-mip)
assert_equals( assert_equals(

@ -12,7 +12,7 @@ from miplearn.classifiers import Classifier
from miplearn.classifiers.threshold import Threshold from miplearn.classifiers.threshold import Threshold
from miplearn.components import classifier_evaluation_dict from miplearn.components import classifier_evaluation_dict
from miplearn.components.primal import PrimalSolutionComponent from miplearn.components.primal import PrimalSolutionComponent
from miplearn.features import TrainingSample, VariableFeatures, Features from miplearn.features import TrainingSample, Variable, Features
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.problems.tsp import TravelingSalesmanGenerator from miplearn.problems.tsp import TravelingSalesmanGenerator
from miplearn.solvers.learning import LearningSolver from miplearn.solvers.learning import LearningSolver
@ -21,18 +21,18 @@ from miplearn.solvers.learning import LearningSolver
def test_xy() -> None: def test_xy() -> None:
features = Features( features = Features(
variables={ variables={
"x[0]": VariableFeatures( "x[0]": Variable(
category="default", category="default",
user_features=[0.0, 0.0], user_features=[0.0, 0.0],
), ),
"x[1]": VariableFeatures( "x[1]": Variable(
category=None, category=None,
), ),
"x[2]": VariableFeatures( "x[2]": Variable(
category="default", category="default",
user_features=[1.0, 0.0], user_features=[1.0, 0.0],
), ),
"x[3]": VariableFeatures( "x[3]": Variable(
category="default", category="default",
user_features=[1.0, 1.0], user_features=[1.0, 1.0],
), ),
@ -78,18 +78,18 @@ def test_xy() -> None:
def test_xy_without_lp_solution() -> None: def test_xy_without_lp_solution() -> None:
features = Features( features = Features(
variables={ variables={
"x[0]": VariableFeatures( "x[0]": Variable(
category="default", category="default",
user_features=[0.0, 0.0], user_features=[0.0, 0.0],
), ),
"x[1]": VariableFeatures( "x[1]": Variable(
category=None, category=None,
), ),
"x[2]": VariableFeatures( "x[2]": Variable(
category="default", category="default",
user_features=[1.0, 0.0], user_features=[1.0, 0.0],
), ),
"x[3]": VariableFeatures( "x[3]": Variable(
category="default", category="default",
user_features=[1.0, 1.0], user_features=[1.0, 1.0],
), ),
@ -141,15 +141,15 @@ def test_predict() -> None:
thr.predict = Mock(return_value=[0.75, 0.75]) thr.predict = Mock(return_value=[0.75, 0.75])
features = Features( features = Features(
variables={ variables={
"x[0]": VariableFeatures( "x[0]": Variable(
category="default", category="default",
user_features=[0.0, 0.0], user_features=[0.0, 0.0],
), ),
"x[1]": VariableFeatures( "x[1]": Variable(
category="default", category="default",
user_features=[0.0, 2.0], user_features=[0.0, 2.0],
), ),
"x[2]": VariableFeatures( "x[2]": Variable(
category="default", category="default",
user_features=[2.0, 0.0], user_features=[2.0, 0.0],
), ),
@ -235,11 +235,11 @@ def test_evaluate() -> None:
} }
features: Features = Features( features: Features = Features(
variables={ variables={
"x[0]": VariableFeatures(), "x[0]": Variable(),
"x[1]": VariableFeatures(), "x[1]": Variable(),
"x[2]": VariableFeatures(), "x[2]": Variable(),
"x[3]": VariableFeatures(), "x[3]": Variable(),
"x[4]": VariableFeatures(), "x[4]": Variable(),
} }
) )
instance = Mock(spec=Instance) instance = Mock(spec=Instance)

@ -5,7 +5,7 @@
from miplearn.features import ( from miplearn.features import (
FeaturesExtractor, FeaturesExtractor,
InstanceFeatures, InstanceFeatures,
VariableFeatures, Variable,
Constraint, Constraint,
) )
from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.gurobi import GurobiSolver
@ -19,19 +19,19 @@ def test_knapsack() -> None:
solver.set_instance(instance, model) solver.set_instance(instance, model)
FeaturesExtractor(solver).extract(instance) FeaturesExtractor(solver).extract(instance)
assert instance.features.variables == { assert instance.features.variables == {
"x[0]": VariableFeatures( "x[0]": Variable(
category="default", category="default",
user_features=[23.0, 505.0], user_features=[23.0, 505.0],
), ),
"x[1]": VariableFeatures( "x[1]": Variable(
category="default", category="default",
user_features=[26.0, 352.0], user_features=[26.0, 352.0],
), ),
"x[2]": VariableFeatures( "x[2]": Variable(
category="default", category="default",
user_features=[20.0, 458.0], user_features=[20.0, 458.0],
), ),
"x[3]": VariableFeatures( "x[3]": Variable(
category="default", category="default",
user_features=[18.0, 220.0], user_features=[18.0, 220.0],
), ),

Loading…
Cancel
Save