diff --git a/discrete_optimization/coloring/solvers/coloring_lp_solvers.py b/discrete_optimization/coloring/solvers/coloring_lp_solvers.py index 995835f1..cef5fa1f 100644 --- a/discrete_optimization/coloring/solvers/coloring_lp_solvers.py +++ b/discrete_optimization/coloring/solvers/coloring_lp_solvers.py @@ -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 @@ -27,7 +29,6 @@ Problem, Solution, ) -from discrete_optimization.generic_tools.do_solver import WarmstartMixin from discrete_optimization.generic_tools.hyperparameters.hyperparameter import ( CategoricalHyperparameter, ) @@ -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: @@ -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, @@ -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): @@ -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 diff --git a/discrete_optimization/facility/solvers/facility_lp_solver.py b/discrete_optimization/facility/solvers/facility_lp_solver.py index 8a9491f6..ba7ee21f 100644 --- a/discrete_optimization/facility/solvers/facility_lp_solver.py +++ b/discrete_optimization/facility/solvers/facility_lp_solver.py @@ -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 @@ -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, @@ -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: @@ -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, @@ -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): diff --git a/discrete_optimization/generic_tools/lp_tools.py b/discrete_optimization/generic_tools/lp_tools.py index 3d7a6ef9..8ddcffbf 100644 --- a/discrete_optimization/generic_tools/lp_tools.py +++ b/discrete_optimization/generic_tools/lp_tools.py @@ -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 @@ -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() @@ -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 @@ -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): diff --git a/discrete_optimization/knapsack/solvers/lp_solvers.py b/discrete_optimization/knapsack/solvers/lp_solvers.py index 51220b0b..b93c4a97 100644 --- a/discrete_optimization/knapsack/solvers/lp_solvers.py +++ b/discrete_optimization/knapsack/solvers/lp_solvers.py @@ -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 @@ -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, @@ -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") @@ -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): diff --git a/discrete_optimization/maximum_independent_set/solvers/mis_gurobi.py b/discrete_optimization/maximum_independent_set/solvers/mis_gurobi.py index c86419be..d5589e25 100644 --- a/discrete_optimization/maximum_independent_set/solvers/mis_gurobi.py +++ b/discrete_optimization/maximum_independent_set/solvers/mis_gurobi.py @@ -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 @@ -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): @@ -66,13 +68,17 @@ 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" ) @@ -80,4 +86,6 @@ def init_model(self, **kwargs: Any) -> None: 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 + ) diff --git a/discrete_optimization/maximum_independent_set/solvers/mis_mathopt.py b/discrete_optimization/maximum_independent_set/solvers/mis_mathopt.py index c2ec8566..6d01f9d2 100644 --- a/discrete_optimization/maximum_independent_set/solvers/mis_mathopt.py +++ b/discrete_optimization/maximum_independent_set/solvers/mis_mathopt.py @@ -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 diff --git a/discrete_optimization/pickup_vrp/solver/lp_solver.py b/discrete_optimization/pickup_vrp/solver/lp_solver.py index 4086c76c..242349d6 100644 --- a/discrete_optimization/pickup_vrp/solver/lp_solver.py +++ b/discrete_optimization/pickup_vrp/solver/lp_solver.py @@ -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 @@ -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, @@ -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 @@ -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", diff --git a/discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py b/discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py index 0b7f7637..c96bbfdf 100644 --- a/discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py +++ b/discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py @@ -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, Hashable from itertools import product @@ -14,7 +16,6 @@ ParamsObjectiveFunction, Solution, ) -from discrete_optimization.generic_tools.do_solver import WarmstartMixin from discrete_optimization.generic_tools.hyperparameters.hyperparameter import ( CategoricalHyperparameter, EnumHyperparameter, @@ -530,13 +531,17 @@ def convert_to_variable_values( class _BaseLP_MRCPSP(MilpSolver, SolverRCPSP): problem: RCPSPModel - x: dict[tuple[Hashable, int, int], Any] hyperparameters = [ CategoricalHyperparameter( name="greedy_start", choices=[True, False], default=True ) ] + x: dict[tuple[Hashable, int, int], Any] + max_horizon: Optional[int] = None + partial_solution: Optional[PartialSolution] = None + hinted_values: dict[Any, float] + def __init__( self, problem: RCPSPModel, @@ -573,6 +578,18 @@ def retrieve_current_solution( rcpsp_schedule_feasible=True, ) + def convert_to_variable_values(self, solution: RCPSPSolution) -> dict[Any, float]: + """Convert a solution to a mapping between model variables and their values. + + Will be used by set_warm_start(). + """ + self.init_model( + max_horizon=self.max_horizon, + partial_solution=self.partial_solution, + start_solution=solution, + ) + return self.hinted_values + class LP_MRCPSP(PymipMilpSolver, _BaseLP_MRCPSP): hyperparameters = LP_RCPSP.hyperparameters @@ -815,12 +832,9 @@ def solve( ) -class LP_MRCPSP_GUROBI(GurobiMilpSolver, _BaseLP_MRCPSP, WarmstartMixin): +class LP_MRCPSP_GUROBI(GurobiMilpSolver, _BaseLP_MRCPSP): hyperparameters = _BaseLP_MRCPSP.hyperparameters - max_horizon: Optional[int] = None - partial_solution: Optional[PartialSolution] = None - def init_model(self, **args): args = self.complete_with_default_hyperparameters(args) greedy_start = args["greedy_start"] @@ -964,7 +978,7 @@ def init_model(self, **args): for (j, s) in S ) - start = [] + hinted_values_tuple_list = [] self.starts = {} for task in sorted_tasks: self.starts[task] = self.model.addVar( @@ -974,9 +988,12 @@ def init_model(self, **args): ub=self.index_time[-1], ) if task in self.start_solution.rcpsp_schedule: - self.starts[task].start = self.start_solution.rcpsp_schedule[task][ - "start_time" - ] + hinted_values_tuple_list.append( + ( + self.starts[task], + self.start_solution.rcpsp_schedule[task]["start_time"], + ) + ) self.model.addLConstr( gurobi.quicksum( [self.x[key] * key[2] for key in variable_per_task[task]] @@ -986,20 +1003,19 @@ def init_model(self, **args): modes_dict = self.problem.build_mode_dict(self.start_solution.rcpsp_modes) for j in self.start_solution.rcpsp_schedule: start_time_j = self.start_solution.rcpsp_schedule[j]["start_time"] - start += [ + hinted_values_tuple_list.append( ( self.durations[j], self.problem.mode_details[j][modes_dict[j]]["duration"], ) - ] + ) + for k in self.variable_per_task[j]: task, mode, time = k if start_time_j == time and mode == modes_dict[j]: - start += [(self.x[k], 1)] - self.x[k].start = 1 + hinted_values_tuple_list.append((self.x[k], 1)) else: - start += [(self.x[k], 0)] - self.x[k].start = 0 + hinted_values_tuple_list.append((self.x[k], 0)) p_s: Optional[PartialSolution] = args.get("partial_solution", None) self.partial_solution = p_s @@ -1082,13 +1098,17 @@ def init_model(self, **args): f"Partial solution constraints : {self.constraints_partial_solutions}" ) self.model.update() + # take into account "warmstart" w/o calling set_warmstart (would cause a recursion issue here) + self.hinted_values = dict(hinted_values_tuple_list) + self.set_warm_start_from_values(variable_values=self.hinted_values) - def set_warm_start(self, solution: RCPSPSolution) -> None: - self.init_model( - max_horizon=self.max_horizon, - partial_solution=self.partial_solution, - start_solution=solution, - ) + def convert_to_variable_values(self, solution: RCPSPSolution) -> dict[Var, float]: + """Convert a solution to a mapping between model variables and their values. + + Will be used by set_warm_start(). + + """ + return _BaseLP_MRCPSP.convert_to_variable_values(self, solution) class LP_MRCPSP_MATHOPT(OrtoolsMathOptMilpSolver, _BaseLP_MRCPSP): @@ -1241,7 +1261,6 @@ def init_model(self, **args): >= durations[j] ) - start = [] self.starts = {} for task in sorted_tasks: self.starts[task] = self.model.add_integer_variable( @@ -1262,19 +1281,14 @@ def init_model(self, **args): modes_dict = self.problem.build_mode_dict(self.start_solution.rcpsp_modes) for j in self.start_solution.rcpsp_schedule: start_time_j = self.start_solution.rcpsp_schedule[j]["start_time"] - start += [ - ( - self.durations[j], - self.problem.mode_details[j][modes_dict[j]]["duration"], - ) - ] + self.hinted_values[self.durations[j]] = self.problem.mode_details[j][ + modes_dict[j] + ]["duration"] for k in self.variable_per_task[j]: task, mode, time = k if start_time_j == time and mode == modes_dict[j]: - start += [(self.x[k], 1)] self.hinted_values[self.x[k]] = 1 else: - start += [(self.x[k], 0)] self.hinted_values[self.x[k]] = 0 p_s: Optional[PartialSolution] = args.get("partial_solution", None) @@ -1361,20 +1375,18 @@ def init_model(self, **args): logger.debug( f"Partial solution constraints : {self.constraints_partial_solutions}" ) - # take into account "warmstart" w/o calling set_warmstart (would cause a recursion issue here) - self.solution_hint = mathopt.SolutionHint( - variable_values=self.hinted_values - ) + # take into account "warmstart" w/o calling set_warmstart (would cause a recursion issue here) + self.set_warm_start_from_values(variable_values=self.hinted_values) def convert_to_variable_values( self, solution: RCPSPSolution ) -> dict[mathopt.Variable, float]: - self.init_model( - max_horizon=self.max_horizon, - partial_solution=self.partial_solution, - start_solution=solution, - ) - return self.hinted_values + """Convert a solution to a mapping between model variables and their values. + + Will be used by set_warm_start(). + + """ + return _BaseLP_MRCPSP.convert_to_variable_values(self, solution) class LP_RCPSP_CPLEX(CplexMilpSolver, _BaseLP_MRCPSP): diff --git a/discrete_optimization/rcpsp/solver/rcpsp_lp_solver_gantt.py b/discrete_optimization/rcpsp/solver/rcpsp_lp_solver_gantt.py index 34795304..82e826f2 100644 --- a/discrete_optimization/rcpsp/solver/rcpsp_lp_solver_gantt.py +++ b/discrete_optimization/rcpsp/solver/rcpsp_lp_solver_gantt.py @@ -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 import random from collections.abc import Callable @@ -13,6 +15,7 @@ from discrete_optimization.generic_tools.do_problem import ( ModeOptim, ParamsObjectiveFunction, + Solution, ) from discrete_optimization.generic_tools.lp_tools import ( GurobiMilpSolver, @@ -254,6 +257,13 @@ def init_model(self, **args): # gurobi solver which is usefull to get a pool of solution (indeed, using the other one we dont have usually a lot of # solution since we converge rapidly to the "optimum" (we don't have an objective value..) class LP_MRCPSP_GANTT_GUROBI(GurobiMilpSolver, _Base_LP_MRCPSP_GANTT): + def convert_to_variable_values( + self, solution: Solution + ) -> dict[gurobipy.Var, float]: + raise NotImplementedError( + f"No warmstart implemented for {self.__class__.__name__}." + ) + def init_model(self, **args): self.model = gurobi.Model("Gantt") self.ressource_id_usage = { diff --git a/notebooks/Knapsack tutorial.ipynb b/notebooks/Knapsack tutorial.ipynb index a26828be..968bbb92 100644 --- a/notebooks/Knapsack tutorial.ipynb +++ b/notebooks/Knapsack tutorial.ipynb @@ -752,7 +752,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.18" + "version": "3.12.2" }, "toc": { "base_numbering": 1, diff --git a/tests/maximum_independent_set/test_maximum_independent_set.py b/tests/maximum_independent_set/test_maximum_independent_set.py index 34326170..fe56894c 100644 --- a/tests/maximum_independent_set/test_maximum_independent_set.py +++ b/tests/maximum_independent_set/test_maximum_independent_set.py @@ -117,6 +117,28 @@ def test_solver_gurobi(): assert result_storage[0][0].chosen == start_solution.chosen +@pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") +def test_solver_quad_gurobi(): + small_example = [f for f in get_data_available() if "1dc.64" in f][0] + mis_model: MisProblem = dimacs_parser_nx(small_example) + + solver = MisQuadraticSolver(mis_model) + result_storage = solver.solve() + + # test warm start + start_solver = MisMilpSolver(mis_model) + start_solution = start_solver.solve().get_best_solution() + + # first solution is not start_solution + assert result_storage[0][0].chosen != start_solution.chosen + + # warm start at first solution + solver.set_warm_start(start_solution) + # force first solution to be the hinted one + result_storage = solver.solve() + assert result_storage[0][0].chosen == start_solution.chosen + + def test_solver_mathopt(): small_example = [f for f in get_data_available() if "1dc.64" in f][0] mis_model: MisProblem = dimacs_parser_nx(small_example) @@ -130,6 +152,10 @@ def test_solver_mathopt(): sol, fit = result_storage.get_best_solution_fit(satisfying=mis_model) assert isinstance(sol, MisSolution) + # test warm start with no assert (sometimes mathopt start from a feasible solution + # but provides only the next optimized solution in results) + solver.set_warm_start(sol) + def test_solver_quad_mathopt(): small_example = [f for f in get_data_available() if "1dc.64" in f][0] @@ -143,3 +169,7 @@ def test_solver_quad_mathopt(): sol, fit = result_storage.get_best_solution_fit(satisfying=mis_model) assert isinstance(sol, MisSolution) + + # test warm start with no assert (sometimes mathopt start from a feasible solution + # but provides only the next optimized solution in results) + solver.set_warm_start(sol) diff --git a/tests/rcpsp_multiskill/test_rcpsp_ms_lp_mathopt.py b/tests/rcpsp_multiskill/test_rcpsp_ms_lp_mathopt.py index fa915ae3..124f8bb1 100644 --- a/tests/rcpsp_multiskill/test_rcpsp_ms_lp_mathopt.py +++ b/tests/rcpsp_multiskill/test_rcpsp_ms_lp_mathopt.py @@ -1,6 +1,12 @@ # 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. +import random + +import numpy as np +import pytest +from ortools.math_opt.python import mathopt + from discrete_optimization.generic_tools.lp_tools import MilpSolverName from discrete_optimization.rcpsp_multiskill.rcpsp_multiskill import ( Employee, @@ -18,7 +24,15 @@ ) -def test_lp(): +@pytest.fixture() +def random_seed(): + seed = 42 + random.seed(seed) + np.random.seed(seed) + return seed + + +def test_lp(random_seed): skills_set: set[str] = {"S1", "S2", "S3"} resources_set: set[str] = {"R1", "R2", "R3"} non_renewable_resources = set() @@ -59,17 +73,22 @@ def test_lp(): horizon=100, horizon_multiplier=1, ) + kwargs_solver = dict( + parameters_milp=ParametersMilp.default(), + mathopt_additional_solve_parameters=mathopt.SolveParameters( + random_seed=random_seed + ), + ) lp_solver = LP_Solver_MRSCPSP_MathOpt(problem=model) - lp_solver.init_model() - - res = lp_solver.solve(parameters_milp=ParametersMilp.default()) + lp_solver.init_model(**kwargs_solver) + res = lp_solver.solve(**kwargs_solver) sol = res.get_best_solution() assert model.satisfy(sol) # test warm start # start solution - start_solver = CPSatMSRCPSPSolver(problem=model) - start_solution = start_solver.solve(time_limit=10).get_best_solution() + assert len(res) > 1 + start_solution, start_fit = res[1] assert model.satisfy(start_solution) # check different from first solution found @@ -80,8 +99,10 @@ def test_lp(): ) # solve with warm_start + lp_solver = LP_Solver_MRSCPSP_MathOpt(problem=model) + lp_solver.init_model(**kwargs_solver) lp_solver.set_warm_start(start_solution) - res2 = lp_solver.solve(parameters_milp=ParametersMilp.default()) + res2 = lp_solver.solve(**kwargs_solver) # check first solution is the warmstart assert (