Skip to content

Commit

Permalink
Unify set_warm_start for lp solvers (gurobi and ortools.mathopt)
Browse files Browse the repository at this point in the history
  • Loading branch information
nhuet committed Sep 24, 2024
1 parent bd5fb13 commit 912da0d
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 100 deletions.
27 changes: 13 additions & 14 deletions discrete_optimization/coloring/solvers/coloring_lp_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from __future__ import annotations

import logging
from collections.abc import Callable, Hashable
from typing import Any, Optional, TypedDict, Union
Expand All @@ -27,7 +29,6 @@
Problem,
Solution,
)
from discrete_optimization.generic_tools.do_solver import WarmstartMixin
from discrete_optimization.generic_tools.hyperparameters.hyperparameter import (
CategoricalHyperparameter,
)
Expand Down Expand Up @@ -171,7 +172,7 @@ def get_range_color(self, node_name, range_color_subset, range_color_all):
return range_color_all


class ColoringLP(GurobiMilpSolver, _BaseColoringLP, WarmstartMixin):
class ColoringLP(GurobiMilpSolver, _BaseColoringLP):
"""Coloring LP solver based on gurobipy library.
Attributes:
Expand Down Expand Up @@ -310,7 +311,7 @@ def init_model(self, **kwargs: Any) -> None:
color_model.setParam("MIPGap", 0.001)
color_model.setParam("Heuristics", 0.01)
self.model = color_model
self.variable_decision = {"colors_var": colors_var}
self.variable_decision = {"colors_var": colors_var, "nb_colors": opt}
self.constraints_dict = {
"one_color_constraints": one_color_constraints,
"constraints_neighbors": constraints_neighbors,
Expand All @@ -329,16 +330,15 @@ def init_model(self, **kwargs: Any) -> None:
"descr": "no neighbors can have same color"
}

def set_warm_start(self, solution: ColoringSolution) -> None:
"""Make the solver warm start from the given solution."""
# Init all variables to 0
for var in self.variable_decision["colors_var"].values():
var.Start = 0
# Set var(node, color) to 1 according to the solution
for i, color in enumerate(solution.colors):
node = self.index_to_nodes_name[i]
variable_decision_key = (node, color)
self.variable_decision["colors_var"][variable_decision_key].Start = 1
def convert_to_variable_values(
self, solution: ColoringSolution
) -> dict[Var, float]:
"""Convert a solution to a mapping between model variables and their values.
Will be used by set_warm_start().
"""
return _BaseColoringLP.convert_to_variable_values(self, solution)


class ColoringLP_MIP(PymipMilpSolver, _BaseColoringLP):
Expand Down Expand Up @@ -524,7 +524,6 @@ class ColoringLPMathOpt(OrtoolsMathOptMilpSolver, _BaseColoringLP):
hyperparameters = _BaseColoringLP.hyperparameters

problem: ColoringProblem
solution_hint: Optional[dict[mathopt.Variable, float]] = None

def convert_to_variable_values(
self, solution: ColoringSolution
Expand Down
26 changes: 14 additions & 12 deletions discrete_optimization/facility/solvers/facility_lp_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from __future__ import annotations

import logging
from collections.abc import Callable
from typing import Any, Optional, Union
Expand All @@ -22,8 +24,8 @@
from discrete_optimization.generic_tools.do_problem import (
ParamsObjectiveFunction,
Problem,
Solution,
)
from discrete_optimization.generic_tools.do_solver import WarmstartMixin
from discrete_optimization.generic_tools.hyperparameters.hyperparameter import (
CategoricalHyperparameter,
IntegerHyperparameter,
Expand Down Expand Up @@ -207,7 +209,7 @@ def retrieve_current_solution(
return FacilitySolution(self.problem, facility_for_customer)


class LP_Facility_Solver(GurobiMilpSolver, _LPFacilitySolverBase, WarmstartMixin):
class LP_Facility_Solver(GurobiMilpSolver, _LPFacilitySolverBase):
"""Milp solver using gurobi library
Attributes:
Expand Down Expand Up @@ -294,7 +296,7 @@ def init_model(self, **kwargs: Any) -> None:
s.setParam("MIPGapAbs", 0.00001)
s.setParam("MIPGap", 0.00000001)
self.model = s
self.variable_decision = {"x": x}
self.variable_decision = {"x": x, "y": used}
self.constraints_dict = {
"constraint_customer": constraints_customer,
"constraint_capacity": constraint_capacity,
Expand All @@ -310,15 +312,15 @@ def init_model(self, **kwargs: Any) -> None:
}
logger.info("Initialized")

def set_warm_start(self, solution: FacilitySolution) -> None:
"""Make the solver warm start from the given solution."""
# Init all variables to 0
for var in self.variable_decision["x"].values():
var.Start = 0
# Set var(facility, customer) to 1 according to the solution
for c, f in enumerate(solution.facility_for_customers):
variable_decision_key = (f, c)
self.variable_decision["x"][variable_decision_key].Start = 1
def convert_to_variable_values(
self, solution: Solution
) -> dict[gurobipy.Var, float]:
"""Convert a solution to a mapping between model variables and their values.
Will be used by set_warm_start().
"""
return _LPFacilitySolverBase.convert_to_variable_values(self, solution)


class LP_Facility_Solver_MathOpt(OrtoolsMathOptMilpSolver, _LPFacilitySolverBase):
Expand Down
51 changes: 49 additions & 2 deletions discrete_optimization/generic_tools/lp_tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright (c) 2022 AIRBUS and its affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import annotations

import copy
import datetime
import logging
Expand Down Expand Up @@ -329,11 +331,24 @@ def convert_to_dual_values(
return dict()

def set_warm_start(self, solution: Solution) -> None:
self.solution_hint = mathopt.SolutionHint(
"""Make the solver warm start from the given solution."""
self.set_warm_start_from_values(
variable_values=self.convert_to_variable_values(solution),
dual_values=self.convert_to_dual_values(solution),
)

def set_warm_start_from_values(
self,
variable_values: dict[mathopt.Variable, float],
dual_values: Optional[dict[mathopt.LinearConstraint, float]] = None,
) -> None:
if dual_values is None:
dual_values = {}
self.solution_hint = mathopt.SolutionHint(
variable_values=variable_values,
dual_values=dual_values,
)

def get_var_value_for_ith_solution(self, var: Any, i: int) -> float:
raise NotImplementedError()

Expand Down Expand Up @@ -543,7 +558,7 @@ def __call__(self, callback_data: mathopt.CallbackData) -> mathopt.CallbackResul
return mathopt.CallbackResult(terminate=stopping)


class GurobiMilpSolver(MilpSolver):
class GurobiMilpSolver(MilpSolver, WarmstartMixin):
"""Milp solver wrapping a solver from gurobi library."""

model: Optional["gurobipy.Model"] = None
Expand Down Expand Up @@ -668,6 +683,38 @@ def nb_solutions(self) -> int:
else:
return self.model.SolCount

@abstractmethod
def convert_to_variable_values(
self, solution: Solution
) -> dict[gurobipy.Var, float]:
"""Convert a solution to a mapping between model variables and their values.
Will be used by `set_warm_start()`.
Override it in subclasses to have a proper warm start. You can also override
`set_warm_start()` if default behaviour is not sufficient.
"""
return {}

def set_warm_start(self, solution: Solution) -> None:
"""Make the solver warm start from the given solution.
By default, this is using `convert_to_variable_values()`. If not sufficient,
you can override it. (And for instance make implementation of `convert_to_variable_values()`
raise a `NotImplementedError`.)
"""
self.set_warm_start_from_values(
variable_values=self.convert_to_variable_values(solution),
)

def set_warm_start_from_values(
self, variable_values: dict[gurobipy.Var, float]
) -> None:
for var, val in variable_values.items():
var.Start = val


class GurobiCallback:
def __init__(self, do_solver: GurobiMilpSolver, callback: Callback):
Expand Down
21 changes: 13 additions & 8 deletions discrete_optimization/knapsack/solvers/lp_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from __future__ import annotations

import logging
from collections.abc import Callable
from typing import Any, Optional, Union
Expand All @@ -16,7 +18,7 @@
ParamsObjectiveFunction,
Solution,
)
from discrete_optimization.generic_tools.do_solver import ResultStorage, WarmstartMixin
from discrete_optimization.generic_tools.do_solver import ResultStorage
from discrete_optimization.generic_tools.lp_tools import (
GurobiMilpSolver,
MilpSolver,
Expand Down Expand Up @@ -113,7 +115,7 @@ def retrieve_current_solution(
)


class LPKnapsackGurobi(GurobiMilpSolver, _BaseLPKnapsack, WarmstartMixin):
class LPKnapsackGurobi(GurobiMilpSolver, _BaseLPKnapsack):
def init_model(self, **kwargs: Any) -> None:
warm_start = kwargs.get("warm_start", {})
self.model = Model("Knapsack")
Expand Down Expand Up @@ -155,12 +157,15 @@ def init_model(self, **kwargs: Any) -> None:
self.model.setParam("MIPGapAbs", 0.00001)
self.model.setParam("MIPGap", 0.00000001)

def set_warm_start(self, solution: KnapsackSolution) -> None:
"""Make the solver warm start from the given solution."""
for i, variable_decision_key in enumerate(sorted(self.variable_decision["x"])):
self.variable_decision["x"][
variable_decision_key
].Start = solution.list_taken[i]
def convert_to_variable_values(
self, solution: KnapsackSolution
) -> dict[Var, float]:
"""Convert a solution to a mapping between model variables and their values.
Will be used by set_warm_start().
"""
return _BaseLPKnapsack.convert_to_variable_values(self, solution)


class LPKnapsackMathOpt(OrtoolsMathOptMilpSolver, _BaseLPKnapsack):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import networkx as nx
import numpy as np

from discrete_optimization.generic_tools.do_solver import WarmstartMixin
from discrete_optimization.generic_tools.graph_api import get_node_attributes
from discrete_optimization.maximum_independent_set.solvers.mis_lp import BaseLPMisSolver

Expand All @@ -21,13 +20,16 @@
from discrete_optimization.maximum_independent_set.mis_model import MisSolution


class BaseGurobiMisSolver(GurobiMilpSolver, BaseLPMisSolver, WarmstartMixin):
class BaseGurobiMisSolver(GurobiMilpSolver, BaseLPMisSolver):
vars_node: dict[int, Var]

def set_warm_start(self, solution: MisSolution) -> None:
"""Make the solver warm start from the given solution."""
for i in range(0, self.problem.number_nodes):
self.vars_node[i].Start = solution.chosen[i]
def convert_to_variable_values(self, solution: MisSolution) -> dict[Var, float]:
"""Convert a solution to a mapping between model variables and their values.
Will be used by set_warm_start().
"""
return BaseLPMisSolver.convert_to_variable_values(self, solution)


class MisMilpSolver(BaseGurobiMisSolver):
Expand Down Expand Up @@ -66,18 +68,24 @@ class MisQuadraticSolver(BaseGurobiMisSolver):
if there is weight, it's going to ignore them
"""

@property
def vars_node(self) -> dict[int, Var]:
return dict(enumerate(self.vars_node_matrix.tolist()))

def init_model(self, **kwargs: Any) -> None:

# Create a new model
self.model = Model()

# Create variables
self.vars_node = self.model.addMVar(
self.vars_node_matrix = self.model.addMVar(
self.problem.number_nodes, vtype=GRB.BINARY, name="N"
)

# Set objective
adj = nx.to_numpy_array(self.problem.graph_nx, nodelist=self.problem.nodes)
J = np.identity(self.problem.number_nodes)
A = J - adj
self.model.setObjective(self.vars_node @ A @ self.vars_node, GRB.MAXIMIZE)
self.model.setObjective(
self.vars_node_matrix @ A @ self.vars_node_matrix, GRB.MAXIMIZE
)
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,28 @@ class MisMathOptQuadraticSolver(OrtoolsMathOptMilpSolver, BaseLPMisSolver):
if there is weight, it's going to ignore them
"""

@property
def vars_node(self) -> dict[int, Var]:
return dict(enumerate(self.vars_node_matrix.tolist()))

def init_model(self, **kwargs: Any) -> None:

# Create a new model
self.model = mathopt.Model()

# Create variables
self.vars_node = [
self.model.add_binary_variable(name=f"N{i}")
for i in range(self.problem.number_nodes)
]
self.vars_node_matrix = np.array(
[
self.model.add_binary_variable(name=f"N{i}")
for i in range(self.problem.number_nodes)
]
)

# Set objective
adj = nx.to_numpy_array(self.problem.graph_nx)
J = np.identity(self.problem.number_nodes)
A = J - adj
self.model.maximize(self.vars_node @ A @ self.vars_node)
self.model.maximize(self.vars_node_matrix @ A @ self.vars_node_matrix)

def convert_to_variable_values(
self, solution: MisSolution
Expand Down
18 changes: 16 additions & 2 deletions discrete_optimization/pickup_vrp/solver/lp_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from __future__ import annotations

import json
import logging
import os
Expand All @@ -20,7 +22,6 @@
ParamsObjectiveFunction,
Solution,
)
from discrete_optimization.generic_tools.do_solver import WarmstartMixin
from discrete_optimization.generic_tools.graph_api import Graph
from discrete_optimization.generic_tools.lp_tools import (
GurobiMilpSolver,
Expand Down Expand Up @@ -145,7 +146,7 @@ def retrieve_current_solution(
return results, obj


class LinearFlowSolver(GurobiMilpSolver, SolverPickupVrp, WarmstartMixin):
class LinearFlowSolver(GurobiMilpSolver, SolverPickupVrp):
problem: GPDP
warm_start: Optional[GPDPSolution] = None

Expand All @@ -170,6 +171,19 @@ def set_warm_start(self, solution: GPDPSolution) -> None:
"""
self.warm_start = solution

def convert_to_variable_values(self, solution: Solution) -> dict[grb.Var, float]:
"""
Not used here by set_warm_start().
Args:
solution:
Returns:
"""
raise NotImplementedError

def one_visit_per_node(
self,
model: "grb.Model",
Expand Down
Loading

0 comments on commit 912da0d

Please sign in to comment.