Skip to content

Commit

Permalink
Add constraint handlers for mathopt solvers
Browse files Browse the repository at this point in the history
- add a base class OrtoolsMathOptConstraintHandler managing constraints removal
- add mathopt constraint handlers
   - for each gurobi ones, sharing most of the code
   - for each pymip ones
- add tests for mathopt and gurobi constraint handlers
  • Loading branch information
nhuet committed Oct 4, 2024
1 parent 4c2bd86 commit 22b3300
Show file tree
Hide file tree
Showing 9 changed files with 611 additions and 115 deletions.
107 changes: 79 additions & 28 deletions discrete_optimization/coloring/solvers/coloring_lp_lns_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import random
from collections.abc import Iterable
from enum import Enum
from typing import Any
from typing import Any, Union

from discrete_optimization.coloring.coloring_model import (
ColoringProblem,
Expand All @@ -16,19 +16,26 @@
from discrete_optimization.coloring.solvers.coloring_lp_solvers import (
ColoringLP,
ColoringLP_MIP,
ColoringLPMathOpt,
)
from discrete_optimization.coloring.solvers.greedy_coloring import GreedyColoring
from discrete_optimization.generic_tools.do_problem import (
ParamsObjectiveFunction,
build_aggreg_function_and_params_objective,
)
from discrete_optimization.generic_tools.hyperparameters.hyperparameter import (
FloatHyperparameter,
)
from discrete_optimization.generic_tools.lns_mip import (
GurobiConstraintHandler,
InitialSolution,
OrtoolsMathOptConstraintHandler,
PymipConstraintHandler,
)
from discrete_optimization.generic_tools.lns_tools import ConstraintHandler
from discrete_optimization.generic_tools.lp_tools import (
GurobiMilpSolver,
MilpSolver,
MilpSolverName,
PymipMilpSolver,
)
Expand Down Expand Up @@ -82,8 +89,8 @@ def get_starting_solution(self) -> ResultStorage:
return solver.solve()


class ConstraintHandlerFixColorsGrb(GurobiConstraintHandler):
"""Constraint builder used in LNS+LP (using gurobi solver) for coloring problem.
class _BaseConstraintHandlerFixColors(ConstraintHandler):
"""Base class for constraint builder used in LNS+LP for coloring problem.
This constraint handler is pretty basic, it fixes a fraction_to_fix proportion of nodes color.
Expand All @@ -92,29 +99,27 @@ class ConstraintHandlerFixColorsGrb(GurobiConstraintHandler):
fraction_to_fix (float): float between 0 and 1, representing the proportion of nodes to constrain.
"""

hyperparameters = [
FloatHyperparameter("fraction_to_fix", low=0.0, high=1.0, default=0.9),
]

def __init__(self, problem: ColoringProblem, fraction_to_fix: float = 0.9):
self.problem = problem
self.fraction_to_fix = fraction_to_fix

def adding_constraint_from_results_store(
self, solver: GurobiMilpSolver, result_storage: ResultStorage, **kwargs: Any
self,
solver: Union[ColoringLP, ColoringLPMathOpt],
result_storage: ResultStorage,
**kwargs: Any
) -> Iterable[Any]:
if not isinstance(solver, ColoringLP):
raise ValueError("milp_solver must a ColoringLP for this constraint.")
if solver.model is None:
solver.init_model()
if solver.model is None:
raise RuntimeError(
"milp_solver.model must be not None after calling milp_solver.init_model()"
)
subpart_color = set(
random.sample(
solver.nodes_name,
int(self.fraction_to_fix * solver.number_of_nodes),
)
)
dict_color_fixed = {}
dict_color_start = {}
current_solution = result_storage.get_best_solution()
if current_solution is None:
raise ValueError(
Expand All @@ -129,30 +134,76 @@ def adding_constraint_from_results_store(
"result_storage.get_best_solution().colors " "should not be None."
)
max_color = max(current_solution.colors)
solver.set_warm_start(current_solution)
for n in solver.nodes_name:
dict_color_start[n] = current_solution.colors[solver.index_nodes_name[n]]
if n in subpart_color and dict_color_start[n] <= max_color - 1:
dict_color_fixed[n] = dict_color_start[n]
current_node_color = current_solution.colors[solver.index_nodes_name[n]]
if n in subpart_color and current_node_color <= max_color - 1:
dict_color_fixed[n] = current_node_color
colors_var = solver.variable_decision["colors_var"]
lns_constraint = []
lns_constraints = []
for key in colors_var:
n, c = key
if c == dict_color_start[n]:
colors_var[n, c].start = 1
colors_var[n, c].varhintval = 1
else:
colors_var[n, c].start = 0
colors_var[n, c].varhintval = 0
if n in dict_color_fixed:
if c == dict_color_fixed[n]:
lns_constraint.append(
solver.model.addLConstr(colors_var[key] == 1, name=str((n, c)))
lns_constraints.append(
solver.add_linear_constraint(
colors_var[key] == 1, name=str((n, c))
)
)
else:
lns_constraint.append(
solver.model.addLConstr(colors_var[key] == 0, name=str((n, c)))
lns_constraints.append(
solver.add_linear_constraint(
colors_var[key] == 0, name=str((n, c))
)
)
return lns_constraint
return lns_constraints


class ConstraintHandlerFixColorsGrb(
GurobiConstraintHandler, _BaseConstraintHandlerFixColors
):
"""Constraint builder used in LNS+LP (using gurobi solver) for coloring problem.
This constraint handler is pretty basic, it fixes a fraction_to_fix proportion of nodes color.
Attributes:
problem (ColoringProblem): input coloring problem
fraction_to_fix (float): float between 0 and 1, representing the proportion of nodes to constrain.
"""

def adding_constraint_from_results_store(
self, solver: ColoringLP, result_storage: ResultStorage, **kwargs: Any
) -> Iterable[Any]:
constraints = (
_BaseConstraintHandlerFixColors.adding_constraint_from_results_store(
self, solver=solver, result_storage=result_storage, **kwargs
)
)
solver.model.update()
return constraints


class ConstraintHandlerFixColorsMathOpt(
OrtoolsMathOptConstraintHandler, _BaseConstraintHandlerFixColors
):
"""Constraint builder used in LNS+LP (using mathopt solver) for coloring problem.
This constraint handler is pretty basic, it fixes a fraction_to_fix proportion of nodes color.
Attributes:
problem (ColoringProblem): input coloring problem
fraction_to_fix (float): float between 0 and 1, representing the proportion of nodes to constrain.
"""

def adding_constraint_from_results_store(
self, solver: ColoringLPMathOpt, result_storage: ResultStorage, **kwargs: Any
) -> Iterable[Any]:
constraints = (
_BaseConstraintHandlerFixColors.adding_constraint_from_results_store(
self, solver=solver, result_storage=result_storage, **kwargs
)
)
return constraints


class ConstraintHandlerFixColorsPyMip(PymipConstraintHandler):
Expand Down
134 changes: 132 additions & 2 deletions discrete_optimization/facility/solvers/facility_lp_lns_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import random
from collections.abc import Iterable
from enum import Enum
from typing import Any
from typing import Any, Union

import mip

Expand All @@ -15,6 +15,8 @@
FacilitySolution,
)
from discrete_optimization.facility.solvers.facility_lp_solver import (
LP_Facility_Solver,
LP_Facility_Solver_MathOpt,
LP_Facility_Solver_PyMip,
MilpSolverName,
)
Expand All @@ -30,10 +32,16 @@
EnumHyperparameter,
)
from discrete_optimization.generic_tools.lns_mip import (
GurobiConstraintHandler,
InitialSolution,
OrtoolsMathOptConstraintHandler,
PymipConstraintHandler,
)
from discrete_optimization.generic_tools.lp_tools import PymipMilpSolver
from discrete_optimization.generic_tools.lns_tools import ConstraintHandler
from discrete_optimization.generic_tools.lp_tools import (
GurobiMilpSolver,
PymipMilpSolver,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -177,3 +185,125 @@ def adding_constraint_from_results_store(
solver.model.solver.update()
solver.model.start = start
return lns_constraint


class _BaseConstraintHandlerFacility(ConstraintHandler):
"""Constraint builder used in LNS+LP for coloring problem.
This constraint handler is pretty basic, it fixes a fraction_to_fix proportion of allocation of customer to
facility.
Attributes:
problem (ColoringProblem): input coloring problem
fraction_to_fix (float): float between 0 and 1, representing the proportion of nodes to constrain.
"""

def __init__(
self,
problem: FacilityProblem,
fraction_to_fix: float = 0.9,
):
self.problem = problem
self.fraction_to_fix = fraction_to_fix

def adding_constraint_from_results_store(
self,
solver: Union[LP_Facility_Solver, LP_Facility_Solver_MathOpt],
result_storage: ResultStorage,
**kwargs: Any,
) -> Iterable[Any]:
subpart_customer = set(
random.sample(
range(self.problem.customer_count),
int(self.fraction_to_fix * self.problem.customer_count),
)
)
current_solution = result_storage.get_best_solution()
if current_solution is None:
raise ValueError(
"result_storage.get_best_solution() " "should not be None."
)
if not isinstance(current_solution, FacilitySolution):
raise ValueError(
"result_storage.get_best_solution() " "should be a FacilitySolution."
)
solver.set_warm_start(current_solution)

dict_f_fixed = {}
for c in range(self.problem.customer_count):
if c in subpart_customer:
dict_f_fixed[c] = current_solution.facility_for_customers[c]
x_var = solver.variable_decision["x"]
lns_constraint = []
for key in x_var:
f, c = key
if c in dict_f_fixed:
if f == dict_f_fixed[c]:
if not isinstance(x_var[f, c], int):
lns_constraint.append(
solver.add_linear_constraint(
x_var[key] == 1, name=str((f, c))
)
)
else:
if not isinstance(x_var[f, c], int):
lns_constraint.append(
solver.add_linear_constraint(
x_var[key] == 0, name=str((f, c))
)
)
return lns_constraint


class ConstraintHandlerFacilityGurobi(
GurobiConstraintHandler, _BaseConstraintHandlerFacility
):
"""Constraint builder used in LNS+LP for coloring problem with gurobi solver.
This constraint handler is pretty basic, it fixes a fraction_to_fix proportion of allocation of customer to
facility.
Attributes:
problem (ColoringProblem): input coloring problem
fraction_to_fix (float): float between 0 and 1, representing the proportion of nodes to constrain.
"""

def adding_constraint_from_results_store(
self, solver: LP_Facility_Solver, result_storage: ResultStorage, **kwargs: Any
) -> Iterable[Any]:
constraints = (
_BaseConstraintHandlerFacility.adding_constraint_from_results_store(
self, solver=solver, result_storage=result_storage, **kwargs
)
)
solver.model.update()
return constraints


class ConstraintHandlerFacilityMathOpt(
OrtoolsMathOptConstraintHandler, _BaseConstraintHandlerFacility
):
"""Constraint builder used in LNS+LP for coloring problem with mathopt solver.
This constraint handler is pretty basic, it fixes a fraction_to_fix proportion of allocation of customer to
facility.
Attributes:
problem (ColoringProblem): input coloring problem
fraction_to_fix (float): float between 0 and 1, representing the proportion of nodes to constrain.
"""

def adding_constraint_from_results_store(
self,
solver: LP_Facility_Solver_MathOpt,
result_storage: ResultStorage,
**kwargs: Any,
) -> Iterable[Any]:
constraints = (
_BaseConstraintHandlerFacility.adding_constraint_from_results_store(
self, solver=solver, result_storage=result_storage, **kwargs
)
)
return constraints
12 changes: 12 additions & 0 deletions discrete_optimization/generic_tools/lns_mip.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
GurobiMilpSolver,
MilpSolver,
MilpSolverName,
OrtoolsMathOptMilpSolver,
ParametersMilp,
PymipMilpSolver,
)
Expand All @@ -42,6 +43,17 @@ def remove_constraints_from_previous_iteration(
solver.model.update()


class OrtoolsMathOptConstraintHandler(ConstraintHandler):
def remove_constraints_from_previous_iteration(
self,
solver: OrtoolsMathOptMilpSolver,
previous_constraints: Iterable[Any],
**kwargs: Any,
) -> None:
for cstr in previous_constraints:
solver.model.delete_linear_constraint(cstr)


class PymipConstraintHandler(ConstraintHandler):
def remove_constraints_from_previous_iteration(
self,
Expand Down
Loading

0 comments on commit 22b3300

Please sign in to comment.