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.
285 lines
8.8 KiB
285 lines
8.8 KiB
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
|
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
|
# Released under the modified BSD license. See COPYING.md for more details.
|
|
|
|
import logging
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from overrides import EnforceOverrides
|
|
|
|
from miplearn.features import Constraint, Variable
|
|
from miplearn.instance.base import Instance
|
|
from miplearn.types import (
|
|
IterationCallback,
|
|
LazyCallback,
|
|
BranchPriorities,
|
|
UserCutCallback,
|
|
Solution,
|
|
VariableName,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class LPSolveStats:
|
|
lp_log: Optional[str] = None
|
|
lp_value: Optional[float] = None
|
|
lp_wallclock_time: Optional[float] = None
|
|
|
|
|
|
@dataclass
|
|
class MIPSolveStats:
|
|
mip_lower_bound: Optional[float]
|
|
mip_log: str
|
|
mip_nodes: Optional[int]
|
|
mip_sense: str
|
|
mip_upper_bound: Optional[float]
|
|
mip_wallclock_time: float
|
|
mip_warm_start_value: Optional[float]
|
|
|
|
|
|
class InternalSolver(ABC, EnforceOverrides):
|
|
"""
|
|
Abstract class representing the MIP solver used internally by LearningSolver.
|
|
"""
|
|
|
|
@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`.
|
|
|
|
This method should not permanently modify the problem. That is, subsequent
|
|
calls to `solve` should solve the original MIP, not the LP relaxation.
|
|
|
|
Parameters
|
|
----------
|
|
tee
|
|
If true, prints the solver log to the screen.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def solve(
|
|
self,
|
|
tee: bool = False,
|
|
iteration_cb: Optional[IterationCallback] = None,
|
|
lazy_cb: Optional[LazyCallback] = None,
|
|
user_cut_cb: Optional[UserCutCallback] = None,
|
|
) -> MIPSolveStats:
|
|
"""
|
|
Solves the currently loaded instance. After this method finishes,
|
|
the best solution found can be retrieved by calling `get_solution`.
|
|
|
|
Parameters
|
|
----------
|
|
iteration_cb: IterationCallback
|
|
By default, InternalSolver makes a single call to the native `solve`
|
|
method and returns the result. If an iteration callback is provided
|
|
instead, InternalSolver enters a loop, where `solve` and `iteration_cb`
|
|
are called alternatively. To stop the loop, `iteration_cb` should return
|
|
False. Any other result causes the solver to loop again.
|
|
lazy_cb: LazyCallback
|
|
This function is called whenever the solver finds a new candidate
|
|
solution and can be used to add lazy constraints to the model. Only the
|
|
following operations within the callback are allowed:
|
|
- Querying the value of a variable
|
|
- Querying if a constraint is satisfied
|
|
- Adding a new constraint to the problem
|
|
Additional operations may be allowed by specific subclasses.
|
|
user_cut_cb: UserCutCallback
|
|
This function is called whenever the solver found a new integer-infeasible
|
|
solution and needs to generate cutting planes to cut it off.
|
|
tee: bool
|
|
If true, prints the solver log to the screen.
|
|
"""
|
|
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.
|
|
|
|
The solution should be a dictionary following the same format as the
|
|
one produced by `get_solution`. 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(
|
|
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 fix(self, solution: Solution) -> None:
|
|
"""
|
|
Fixes the values of a subset of decision variables.
|
|
|
|
The values should be provided in the dictionary format generated by
|
|
`get_solution`. 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.
|
|
|
|
The priorities should be provided in the dictionary format generated by
|
|
`get_solution`. Missing values indicate variables whose priorities
|
|
should not be modified.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abstractmethod
|
|
def get_constraints(self) -> Dict[str, Constraint]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def add_constraint(self, constr: Constraint, name: str) -> None:
|
|
"""
|
|
Adds a given constraint to the model.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def remove_constraint(self, name: str) -> None:
|
|
"""
|
|
Removes the constraint that has a given name from the model.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def is_constraint_satisfied(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 get_dual(self, cid: str) -> float:
|
|
"""
|
|
If the model is feasible and has been solved to optimality, returns the
|
|
optimal value of the dual variable associated with this constraint. If the
|
|
model is infeasible, returns a portion of the infeasibility certificate
|
|
corresponding to the given constraint.
|
|
|
|
Only available for relaxed problems. Must be called after solve.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_sense(self) -> str:
|
|
"""
|
|
Returns the sense of the problem (either "min" or "max").
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_variable_names(self) -> List[VariableName]:
|
|
"""
|
|
Returns a list containing the names of all variables in the model. This
|
|
method is used by the ML components to query what variables are there in the
|
|
model before a solution is available.
|
|
"""
|
|
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:
|
|
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) -> Dict[str, Variable]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_constraint_attrs(self) -> List[str]:
|
|
"""
|
|
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
|