From 4bb05f9ce9eb0cd3c3923bb61a4cdf1b50178d66 Mon Sep 17 00:00:00 2001 From: Nolwen Date: Fri, 9 Aug 2024 10:13:36 +0200 Subject: [PATCH] Implement a sequential metasolver - Chain several subsolvers - Next subsolver warm started by the best solution of the previous one* - subsolvers must inherit from WarmstartMixin, except for first one - **kwargs needed in all solvers __init__() method as we apply - subsolver.__init__(problem=problem, **kwargs) - subsolver.init_model(**kwargs) - subsolver.solve(**kwargs) --- .../coloring/solvers/coloring_quantum.py | 4 + .../facility/solvers/gphh_facility.py | 1 + .../facility/solvers/greedy_solvers.py | 1 + .../generic_rcpsp_tools/gphh_solver.py | 1 + .../generic_tools/ea/alternating_ga.py | 1 + discrete_optimization/generic_tools/ea/ga.py | 1 + .../generic_tools/ls/hill_climber.py | 1 + .../generic_tools/ls/simulated_annealing.py | 1 + .../generic_tools/qiskit_tools.py | 1 + .../generic_tools/sequential_metasolver.py | 91 +++++++++++++++++ .../solvers/mis_quantum.py | 2 + discrete_optimization/rcpsp/solver/cpm.py | 1 + .../solvers/multimode_transposition.py | 1 + .../tsp/solver/tsp_cp_solver.py | 1 + .../tsp/solver/tsp_quantum.py | 2 + tests/rcpsp/test_sequential_metasolver.py | 99 +++++++++++++++++++ 16 files changed, 209 insertions(+) create mode 100644 discrete_optimization/generic_tools/sequential_metasolver.py create mode 100644 tests/rcpsp/test_sequential_metasolver.py diff --git a/discrete_optimization/coloring/solvers/coloring_quantum.py b/discrete_optimization/coloring/solvers/coloring_quantum.py index 671082776..ebab1d03a 100644 --- a/discrete_optimization/coloring/solvers/coloring_quantum.py +++ b/discrete_optimization/coloring/solvers/coloring_quantum.py @@ -150,6 +150,7 @@ def __init__( problem: ColoringProblem, nb_max_color=None, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__(problem, params_objective_function) self.coloring_qiskit = ColoringQiskit_MinimizeNbColor( @@ -169,6 +170,7 @@ def __init__( problem: ColoringProblem, nb_max_color=None, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__(problem, params_objective_function) self.coloring_qiskit = ColoringQiskit_MinimizeNbColor( @@ -258,6 +260,7 @@ def __init__( problem: ColoringProblem, nb_color=None, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__(problem, params_objective_function) self.coloring_qiskit = ColoringQiskit_FeasibleNbColor( @@ -277,6 +280,7 @@ def __init__( problem: ColoringProblem, nb_color=None, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__(problem, params_objective_function) self.coloring_qiskit = ColoringQiskit_FeasibleNbColor( diff --git a/discrete_optimization/facility/solvers/gphh_facility.py b/discrete_optimization/facility/solvers/gphh_facility.py index f52acb52a..a7783c9dc 100644 --- a/discrete_optimization/facility/solvers/gphh_facility.py +++ b/discrete_optimization/facility/solvers/gphh_facility.py @@ -205,6 +205,7 @@ def __init__( weight: int = 1, params_gphh: Optional[ParametersGPHH] = None, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/facility/solvers/greedy_solvers.py b/discrete_optimization/facility/solvers/greedy_solvers.py index b88dfc5da..10f2be5dc 100644 --- a/discrete_optimization/facility/solvers/greedy_solvers.py +++ b/discrete_optimization/facility/solvers/greedy_solvers.py @@ -48,6 +48,7 @@ def __init__( self, problem: FacilityProblem, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/generic_rcpsp_tools/gphh_solver.py b/discrete_optimization/generic_rcpsp_tools/gphh_solver.py index 7a06068ad..3293a5eec 100644 --- a/discrete_optimization/generic_rcpsp_tools/gphh_solver.py +++ b/discrete_optimization/generic_rcpsp_tools/gphh_solver.py @@ -492,6 +492,7 @@ def __init__( weight: int = 1, params_gphh: ParametersGPHH = None, params_objective_function: ParamsObjectiveFunction = None, + **kwargs, ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/generic_tools/ea/alternating_ga.py b/discrete_optimization/generic_tools/ea/alternating_ga.py index 11945ff1a..384a9dec0 100644 --- a/discrete_optimization/generic_tools/ea/alternating_ga.py +++ b/discrete_optimization/generic_tools/ea/alternating_ga.py @@ -58,6 +58,7 @@ def __init__( tournament_size: Optional[float] = None, deap_verbose: bool = False, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/generic_tools/ea/ga.py b/discrete_optimization/generic_tools/ea/ga.py index a749ca997..b8853e059 100644 --- a/discrete_optimization/generic_tools/ea/ga.py +++ b/discrete_optimization/generic_tools/ea/ga.py @@ -120,6 +120,7 @@ def __init__( deap_verbose: bool = True, initial_population: Optional[List[List[Any]]] = None, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs, ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/generic_tools/ls/hill_climber.py b/discrete_optimization/generic_tools/ls/hill_climber.py index 2734c0d79..34a921c6f 100644 --- a/discrete_optimization/generic_tools/ls/hill_climber.py +++ b/discrete_optimization/generic_tools/ls/hill_climber.py @@ -42,6 +42,7 @@ def __init__( mode_mutation: ModeMutation, params_objective_function: Optional[ParamsObjectiveFunction] = None, store_solution: bool = False, + **kwargs, ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/generic_tools/ls/simulated_annealing.py b/discrete_optimization/generic_tools/ls/simulated_annealing.py index c62c03c7b..0318e8190 100644 --- a/discrete_optimization/generic_tools/ls/simulated_annealing.py +++ b/discrete_optimization/generic_tools/ls/simulated_annealing.py @@ -59,6 +59,7 @@ def __init__( mode_mutation: ModeMutation, params_objective_function: Optional[ParamsObjectiveFunction] = None, store_solution: bool = False, + **kwargs, ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/generic_tools/qiskit_tools.py b/discrete_optimization/generic_tools/qiskit_tools.py index 8501602d9..4723c6fd8 100644 --- a/discrete_optimization/generic_tools/qiskit_tools.py +++ b/discrete_optimization/generic_tools/qiskit_tools.py @@ -239,6 +239,7 @@ def __init__( self, problem: Problem, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs, ): super().__init__(problem, params_objective_function) diff --git a/discrete_optimization/generic_tools/sequential_metasolver.py b/discrete_optimization/generic_tools/sequential_metasolver.py new file mode 100644 index 000000000..cc0bbd035 --- /dev/null +++ b/discrete_optimization/generic_tools/sequential_metasolver.py @@ -0,0 +1,91 @@ +import logging +from typing import Any, List, Optional + +from discrete_optimization.generic_tools.callbacks.callback import ( + Callback, + CallbackList, +) +from discrete_optimization.generic_tools.do_problem import ( + ParamsObjectiveFunction, + Problem, +) +from discrete_optimization.generic_tools.do_solver import SolverDO, WarmstartMixin +from discrete_optimization.generic_tools.hyperparameters.hyperparameter import SubBrick +from discrete_optimization.generic_tools.result_storage.result_storage import ( + ResultStorage, +) + +logger = logging.getLogger(__name__) + + +class SequentialMetasolver(SolverDO): + """Sequential metasolver. + + The problem will be solved sequentially, each subsolver being warm started by the previous one. + Therefore each subsolver must inherit from WarmstartMixin, except the first one. + + """ + + def __init__( + self, + problem: Problem, + params_objective_function: Optional[ParamsObjectiveFunction] = None, + list_subbricks: Optional[List[SubBrick]] = None, + **kwargs, + ): + """ + + Args: + list_subbricks: list of subsolvers class and kwargs to be used sequentially + + """ + super().__init__( + problem=problem, params_objective_function=params_objective_function + ) + self.list_subbricks = list_subbricks + self.nb_solvers = len(list_subbricks) + + # checks + if len(self.list_subbricks) == 0: + raise ValueError("list_subbricks must contain at least one subbrick.") + for i_subbrick, subbrick in enumerate(self.list_subbricks): + if not issubclass(subbrick.cls, SolverDO): + raise ValueError("Each subsolver must inherit SolverDO.") + if i_subbrick > 0 and not issubclass(subbrick.cls, WarmstartMixin): + raise ValueError( + "Each subsolver except the first one must inherit WarmstartMixin." + ) + + def solve( + self, callbacks: Optional[List[Callback]] = None, **kwargs: Any + ) -> ResultStorage: + # wrap all callbacks in a single one + callbacks_list = CallbackList(callbacks=callbacks) + # start of solve callback + callbacks_list.on_solve_start(solver=self) + + # iterate over next solvers + res_tot = self.create_result_storage() + for i_subbrick, subbrick in enumerate(self.list_subbricks): + subsolver: SolverDO = subbrick.cls(problem=self.problem, **subbrick.kwargs) + subsolver.init_model(**subbrick.kwargs) + if i_subbrick > 0: + subsolver.set_warm_start(res.get_best_solution()) + res = subsolver.solve(**subbrick.kwargs) + res_tot.extend(res) + + # end of step callback: stopping? + stopping = callbacks_list.on_step_end( + step=i_subbrick, res=res_tot, solver=self + ) + if len(res) == 0: + # no solution => warning + stopping if first subsolver + logger.warning(f"Subsolver #{i_subbrick} did not find any solution.") + if i_subbrick == 0: + stopping = True + if stopping: + break + + # end of solve callback + callbacks_list.on_solve_end(res=res_tot, solver=self) + return res_tot diff --git a/discrete_optimization/maximum_independent_set/solvers/mis_quantum.py b/discrete_optimization/maximum_independent_set/solvers/mis_quantum.py index 81fa63a4a..c97502854 100644 --- a/discrete_optimization/maximum_independent_set/solvers/mis_quantum.py +++ b/discrete_optimization/maximum_independent_set/solvers/mis_quantum.py @@ -82,6 +82,7 @@ def __init__( self, problem: MisProblem, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs, ): super().__init__(problem, params_objective_function) self.mis_qiskit = MisQiskit(problem) @@ -98,6 +99,7 @@ def __init__( self, problem: MisProblem, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs, ): super().__init__(problem, params_objective_function) self.mis_qiskit = MisQiskit(problem) diff --git a/discrete_optimization/rcpsp/solver/cpm.py b/discrete_optimization/rcpsp/solver/cpm.py index 151416a5c..a06aa0fbd 100644 --- a/discrete_optimization/rcpsp/solver/cpm.py +++ b/discrete_optimization/rcpsp/solver/cpm.py @@ -51,6 +51,7 @@ def __init__( self, problem: RCPSPModel, params_objective_function: ParamsObjectiveFunction = None, + **kwargs, ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/rcpsp_multiskill/solvers/multimode_transposition.py b/discrete_optimization/rcpsp_multiskill/solvers/multimode_transposition.py index e20859e1c..e5baeb32a 100644 --- a/discrete_optimization/rcpsp_multiskill/solvers/multimode_transposition.py +++ b/discrete_optimization/rcpsp_multiskill/solvers/multimode_transposition.py @@ -46,6 +46,7 @@ def __init__( worker_type_to_worker: Dict[str, Set[Union[str, int]]] = None, params_objective_function: ParamsObjectiveFunction = None, solver_multimode_rcpsp: SolverDO = None, + **kwargs ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/tsp/solver/tsp_cp_solver.py b/discrete_optimization/tsp/solver/tsp_cp_solver.py index d457067b2..85975a6b4 100644 --- a/discrete_optimization/tsp/solver/tsp_cp_solver.py +++ b/discrete_optimization/tsp/solver/tsp_cp_solver.py @@ -35,6 +35,7 @@ def __init__( cp_solver_name: CPSolverName = CPSolverName.CHUFFED, params_objective_function: Optional[ParamsObjectiveFunction] = None, silent_solve_error: bool = False, + **kwargs ): super().__init__( problem=problem, params_objective_function=params_objective_function diff --git a/discrete_optimization/tsp/solver/tsp_quantum.py b/discrete_optimization/tsp/solver/tsp_quantum.py index 400fa7a67..327d91b64 100644 --- a/discrete_optimization/tsp/solver/tsp_quantum.py +++ b/discrete_optimization/tsp/solver/tsp_quantum.py @@ -154,6 +154,7 @@ def __init__( self, problem: TSPModel2D, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__(problem, params_objective_function) self.tsp_qiskit = TSP2dQiskit(problem) @@ -170,6 +171,7 @@ def __init__( self, problem: TSPModel2D, params_objective_function: Optional[ParamsObjectiveFunction] = None, + **kwargs ): super().__init__(problem, params_objective_function) self.tsp_qiskit = TSP2dQiskit(problem) diff --git a/tests/rcpsp/test_sequential_metasolver.py b/tests/rcpsp/test_sequential_metasolver.py new file mode 100644 index 000000000..6e363982c --- /dev/null +++ b/tests/rcpsp/test_sequential_metasolver.py @@ -0,0 +1,99 @@ +# Copyright (c) 2024 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 logging +import random + +import numpy as np +import pytest + +from discrete_optimization.generic_tools.callbacks.early_stoppers import TimerStopper +from discrete_optimization.generic_tools.callbacks.loggers import ( + NbIterationTracker, + ObjectiveLogger, +) +from discrete_optimization.generic_tools.cp_tools import ParametersCP +from discrete_optimization.generic_tools.hyperparameters.hyperparameter import SubBrick +from discrete_optimization.generic_tools.ls.local_search import ( + ModeMutation, + RestartHandlerLimit, +) +from discrete_optimization.generic_tools.ls.simulated_annealing import ( + SimulatedAnnealing, + TemperatureSchedulingFactor, +) +from discrete_optimization.generic_tools.mutations.mixed_mutation import ( + BasicPortfolioMutation, +) +from discrete_optimization.generic_tools.mutations.mutation_catalog import ( + get_available_mutations, +) +from discrete_optimization.generic_tools.sequential_metasolver import ( + SequentialMetasolver, +) +from discrete_optimization.rcpsp.rcpsp_parser import get_data_available, parse_file +from discrete_optimization.rcpsp.solver import PileSolverRCPSP +from discrete_optimization.rcpsp.solver.cpsat_solver import CPSatRCPSPSolver + +logging.basicConfig(level=logging.INFO) + + +@pytest.fixture +def random_seed(): + random.seed(0) + np.random.seed(0) + + +def test_sequential_metasolver_rcpsp(random_seed): + logging.basicConfig(level=logging.INFO) + + files_available = get_data_available() + file = [f for f in files_available if "j1201_1.sm" in f][0] + rcpsp_problem = parse_file(file) + + # kwargs SA + solution = rcpsp_problem.get_dummy_solution() + _, list_mutation = get_available_mutations(rcpsp_problem, solution) + list_mutation = [ + mutate[0].build(rcpsp_problem, solution, **mutate[1]) + for mutate in list_mutation + ] + mixed_mutation = BasicPortfolioMutation( + list_mutation, np.ones((len(list_mutation))) + ) + restart_handler = RestartHandlerLimit(3000) + temperature_handler = TemperatureSchedulingFactor(1000, restart_handler, 0.99) + + # kwargs cpsat + parameters_cp = ParametersCP.default_cpsat() + parameters_cp.time_limit = 20 + parameters_cp.time_limit_iter0 = 20 + + list_subbricks = [ + SubBrick(cls=PileSolverRCPSP, kwargs=dict()), + SubBrick( + cls=SimulatedAnnealing, + kwargs=dict( + mutator=mixed_mutation, + restart_handler=restart_handler, + temperature_handler=temperature_handler, + mode_mutation=ModeMutation.MUTATE, + nb_iteration_max=5000, + ), + ), + SubBrick(cls=CPSatRCPSPSolver, kwargs=dict(parameters_cp=parameters_cp)), + ] + + solver = SequentialMetasolver(problem=rcpsp_problem, list_subbricks=list_subbricks) + result_storage = solver.solve( + callbacks=[ + NbIterationTracker(step_verbosity_level=logging.INFO), + ObjectiveLogger( + step_verbosity_level=logging.INFO, end_verbosity_level=logging.INFO + ), + TimerStopper(total_seconds=30), + ], + ) + solution, fit = result_storage.get_best_solution_fit() + print(solution, fit) + assert rcpsp_problem.satisfy(solution)