diff --git a/discrete_optimization/pickup_vrp/plots/__init__.py b/discrete_optimization/pickup_vrp/plots/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discrete_optimization/pickup_vrp/plots/gpdp_plot_utils.py b/discrete_optimization/pickup_vrp/plots/gpdp_plot_utils.py new file mode 100644 index 000000000..4bbf00c3f --- /dev/null +++ b/discrete_optimization/pickup_vrp/plots/gpdp_plot_utils.py @@ -0,0 +1,50 @@ +# 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. +from __future__ import print_function + +from typing import Tuple + +import matplotlib.pyplot as plt +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from matplotlib.figure import Figure + +from discrete_optimization.pickup_vrp.gpdp import GPDP, GPDPSolution + + +def plot_gpdp_solution( + sol: GPDPSolution, + problem: GPDP, +) -> Tuple[Figure, Axes]: + if problem.coordinates_2d is None: + raise ValueError( + "problem.coordinates_2d cannot be None when calling plot_ortools_solution." + ) + vehicle_tours = sol.trajectories + fig, ax = plt.subplots(1) + nb_colors = problem.number_vehicle + nb_colors_clusters = len(problem.clusters_set) + colors_nodes = plt.cm.get_cmap("hsv", nb_colors_clusters) + ax.scatter( + [problem.coordinates_2d[node][0] for node in problem.clusters_dict], + [problem.coordinates_2d[node][1] for node in problem.clusters_dict], + s=1, + color=[ + colors_nodes(problem.clusters_dict[node]) for node in problem.clusters_dict + ], + ) + for v, traj in vehicle_tours.items(): + ax.plot( + [problem.coordinates_2d[node][0] for node in traj], + [problem.coordinates_2d[node][1] for node in traj], + label="vehicle n°" + str(v), + ) + ax.scatter( + [problem.coordinates_2d[node][0] for node in traj], + [problem.coordinates_2d[node][1] for node in traj], + s=10, + color=[colors_nodes(problem.clusters_dict[node]) for node in traj], + ) + ax.legend() + return fig, ax diff --git a/discrete_optimization/pickup_vrp/solver/lp_solver.py b/discrete_optimization/pickup_vrp/solver/lp_solver.py index cf845b7f1..c846cfea4 100644 --- a/discrete_optimization/pickup_vrp/solver/lp_solver.py +++ b/discrete_optimization/pickup_vrp/solver/lp_solver.py @@ -24,6 +24,10 @@ import matplotlib.pyplot as plt import networkx as nx +from discrete_optimization.generic_tools.callbacks.callback import ( + Callback, + CallbackList, +) from discrete_optimization.generic_tools.do_problem import ( ParamsObjectiveFunction, Solution, @@ -878,8 +882,9 @@ def solve_iterative( nb_iteration_max: int = 10, json_dump_folder: Optional[str] = None, warm_start: Optional[Dict[Any, Any]] = None, + callbacks: Optional[List[Callback]] = None, **kwargs: Any, - ) -> List[TemporaryResult]: + ) -> ResultStorage: """ Args: @@ -893,6 +898,11 @@ def solve_iterative( Returns: """ + # wrap all callbacks in a single one + callbacks_list = CallbackList(callbacks=callbacks) + # start of solve callback + callbacks_list.on_solve_start(solver=self) + if self.model is None: self.init_model(**kwargs) if self.model is None: # for mypy @@ -929,6 +939,14 @@ def solve_iterative( ) else: subtour = SubtourAddingConstraint(problem=self.problem, linear_solver=self) + subtour.adding_component_constraints([first_solution]) + self.model.update() + nb_iteration = 0 + res = ResultStorage( + list_solution_fits=self.convert_temporaryresults(solutions), + mode_optim=self.params_objective_function.sense_function, + ) + if ( max( [ @@ -939,11 +957,12 @@ def solve_iterative( == 1 ): finished = True - return solutions - subtour.adding_component_constraints([first_solution]) - self.model.update() - all_solutions = solutions - nb_iteration = 0 + return res + else: + finished = callbacks_list.on_step_end( + step=nb_iteration, res=res, solver=self + ) + while not finished: rebuilt_dict = first_solution.rebuilt_dict if (json_dump_folder is not None) and all( @@ -986,7 +1005,6 @@ def solve_iterative( "Temporary result attributes rebuilt_dict, component_global" "and connected_components_per_vehicle cannot be None after solving." ) - all_solutions += solutions if self.clusters_version: subtour = SubtourAddingConstraintCluster( problem=self.problem, linear_solver=self @@ -1008,10 +1026,17 @@ def solve_iterative( and not do_lns ): finished = True - return all_solutions - nb_iteration += 1 - finished = nb_iteration > nb_iteration_max - return all_solutions + else: + nb_iteration += 1 + res.list_solution_fits += self.convert_temporaryresults(solutions) + stopping = callbacks_list.on_step_end( + step=nb_iteration, res=res, solver=self + ) + finished = stopping or nb_iteration > nb_iteration_max + + # end of solve callback + callbacks_list.on_solve_end(res=res, solver=self) + return res def solve( self, @@ -1020,33 +1045,30 @@ def solve( nb_iteration_max: int = 10, json_dump_folder: Optional[str] = None, warm_start: Optional[Dict[Any, Any]] = None, + callbacks: Optional[List[Callback]] = None, **kwargs: Any, ) -> ResultStorage: - if parameters_milp is None: - parameters_milp = ParametersMilp.default() - temporaryresults = self.solve_iterative( + return self.solve_iterative( parameters_milp=parameters_milp, do_lns=do_lns, nb_iteration_max=nb_iteration_max, json_dump_folder=json_dump_folder, warm_start=warm_start, + callbacks=callbacks, **kwargs, ) - if parameters_milp.retrieve_all_solution: - n_solutions = min(parameters_milp.n_solutions_max, self.nb_solutions) - else: - n_solutions = 1 + + def convert_temporaryresults( + self, temporary_results: List[TemporaryResult] + ) -> List[Tuple[Solution, Union[float, TupleFitness]]]: list_solution_fits: List[Tuple[Solution, Union[float, TupleFitness]]] = [] - for s in range(n_solutions): + for temporaryresult in temporary_results: solution = convert_temporaryresult_to_gpdpsolution( - temporaryresult=temporaryresults[s], problem=self.problem + temporaryresult=temporaryresult, problem=self.problem ) fit = self.aggreg_from_sol(solution) list_solution_fits.append((solution, fit)) - return ResultStorage( - list_solution_fits=list_solution_fits, - mode_optim=self.params_objective_function.sense_function, - ) + return list_solution_fits def init_warm_start(self, routes: Dict[int, List]) -> None: if routes is None: @@ -1326,8 +1348,9 @@ def solve_iterative( nb_iteration_max: int = 10, json_dump_folder: Optional[str] = None, warm_start: Optional[Dict[Any, Any]] = None, + callbacks: Optional[List[Callback]] = None, **kwargs: Any, - ) -> List[TemporaryResult]: + ) -> ResultStorage: """ Args: @@ -1341,6 +1364,11 @@ def solve_iterative( Returns: """ + # wrap all callbacks in a single one + callbacks_list = CallbackList(callbacks=callbacks) + # start of solve callback + callbacks_list.on_solve_start(solver=self) + if self.model is None: self.init_model(**kwargs) if self.model is None: # for mypy @@ -1371,8 +1399,11 @@ def solve_iterative( "and connected_components_per_vehicle cannot be None after solving." ) self.model.update() - all_solutions = solutions nb_iteration = 0 + res = ResultStorage( + list_solution_fits=self.convert_temporaryresults(solutions), + mode_optim=self.params_objective_function.sense_function, + ) while not finished: rebuilt_dict = first_solution.rebuilt_dict @@ -1404,10 +1435,16 @@ def solve_iterative( solutions = self.solve_one_iteration( parameters_milp=parameters_milp, no_warm_start=True, **kwargs ) - all_solutions += solutions nb_iteration += 1 - finished = nb_iteration > nb_iteration_max - return all_solutions + res.list_solution_fits += self.convert_temporaryresults(solutions) + stopping = callbacks_list.on_step_end( + step=nb_iteration, res=res, solver=self + ) + finished = stopping or nb_iteration > nb_iteration_max + + # end of solve callback + callbacks_list.on_solve_end(res=res, solver=self) + return res class SubtourAddingConstraint: @@ -2433,56 +2470,3 @@ def update_model_lazy( ) lp_solver.model.update() return list_constraints - - -def plot_solution(temporary_result: TemporaryResult, problem: GPDP) -> None: - if problem.coordinates_2d is None: - raise ValueError( - "problem.coordinates_2d cannot be None when calling plot_solution." - ) - if temporary_result.rebuilt_dict is None: - raise ValueError( - "temporary_result.rebuilt_dict cannot be None when calling plot_solution." - ) - - fig, ax = plt.subplots(2) - flow = temporary_result.flow_solution - nb_colors = problem.number_vehicle - colors = plt.cm.get_cmap("hsv", 2 * nb_colors) - nb_colors_clusters = len(problem.clusters_set) - colors_nodes = plt.cm.get_cmap("hsv", nb_colors_clusters) - for node in problem.clusters_dict: - ax[0].scatter( - [problem.coordinates_2d[node][0]], - [problem.coordinates_2d[node][1]], - s=1, - color=colors_nodes(problem.clusters_dict[node]), - ) - for v in flow: - color = colors(v) - for e in flow[v]: - ax[0].plot( - [problem.coordinates_2d[e[0]][0], problem.coordinates_2d[e[1]][0]], - [problem.coordinates_2d[e[0]][1], problem.coordinates_2d[e[1]][1]], - color=color, - ) - ax[0].scatter( - [problem.coordinates_2d[e[0]][0]], - [problem.coordinates_2d[e[0]][1]], - s=10, - color=colors_nodes(problem.clusters_dict[e[0]]), - ) - ax[0].scatter( - [problem.coordinates_2d[e[1]][0]], - [problem.coordinates_2d[e[1]][1]], - s=10, - color=colors_nodes(problem.clusters_dict[e[1]]), - ) - - for v in temporary_result.rebuilt_dict: - color = colors(v) - ax[1].plot( - [problem.coordinates_2d[n][0] for n in temporary_result.rebuilt_dict[v]], - [problem.coordinates_2d[n][1] for n in temporary_result.rebuilt_dict[v]], - color=color, - ) diff --git a/discrete_optimization/pickup_vrp/solver/lp_solver_pymip.py b/discrete_optimization/pickup_vrp/solver/lp_solver_pymip.py index 19748aba6..3e3026c40 100644 --- a/discrete_optimization/pickup_vrp/solver/lp_solver_pymip.py +++ b/discrete_optimization/pickup_vrp/solver/lp_solver_pymip.py @@ -21,6 +21,10 @@ import mip import networkx as nx +from discrete_optimization.generic_tools.callbacks.callback import ( + Callback, + CallbackList, +) from discrete_optimization.generic_tools.do_problem import ( ParamsObjectiveFunction, Solution, @@ -774,8 +778,16 @@ def solve_one_iteration( return list_temporary_results def solve_iterative( - self, parameters_milp: Optional[ParametersMilp] = None, **kwargs: Any - ) -> List[TemporaryResult]: + self, + parameters_milp: Optional[ParametersMilp] = None, + 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) + if self.model is None: self.init_model(**kwargs) if self.model is None: # for mypy @@ -784,7 +796,6 @@ def solve_iterative( ) if parameters_milp is None: parameters_milp = ParametersMilp.default() - finished = False do_lns = kwargs.get("do_lns", True) reinit_model_at_each_iteration = kwargs.get( "reinit", self.solver_name == mip.CBC @@ -813,6 +824,16 @@ def solve_iterative( ) else: subtour = SubtourAddingConstraint(problem=self.problem, linear_solver=self) + res = ResultStorage( + list_solution_fits=self.convert_temporaryresults(solutions), + mode_optim=self.params_objective_function.sense_function, + ) + list_constraints_tuples: List[Any] = [] + list_constraints_tuples += subtour.adding_component_constraints( + [first_solution] + ) + nb_iteration = 0 + if ( max( [ @@ -822,13 +843,12 @@ def solve_iterative( ) == 1 ): - return solutions - list_constraints_tuples: List[Any] = [] - list_constraints_tuples += subtour.adding_component_constraints( - [first_solution] - ) - all_solutions = solutions - nb_iteration = 0 + finished = True + else: + finished = callbacks_list.on_step_end( + step=nb_iteration, res=res, solver=self + ) + while not finished: rebuilt_dict = first_solution.rebuilt_dict c = ConstraintHandlerOrWarmStart( @@ -848,7 +868,6 @@ def solve_iterative( "Temporary result attributes rebuilt_dict, component_global" "and connected_components_per_vehicle cannot be None after solving." ) - all_solutions += solutions if self.clusters_version: subtour = SubtourAddingConstraintCluster( problem=self.problem, linear_solver=self @@ -875,37 +894,42 @@ def solve_iterative( == 1 and not do_lns ): - return all_solutions - nb_iteration += 1 - finished = nb_iteration > nb_iteration_max - return all_solutions + finished = True + else: + nb_iteration += 1 + res.list_solution_fits += self.convert_temporaryresults(solutions) + stopping = callbacks_list.on_step_end( + step=nb_iteration, res=res, solver=self + ) + finished = stopping or nb_iteration > nb_iteration_max + + # end of solve callback + callbacks_list.on_solve_end(res=res, solver=self) + return res def solve( self, parameters_milp: Optional[ParametersMilp] = None, + callbacks: Optional[List[Callback]] = None, **kwargs: Any, ) -> ResultStorage: - if parameters_milp is None: - parameters_milp = ParametersMilp.default() - temporaryresults = self.solve_iterative( + return self.solve_iterative( parameters_milp=parameters_milp, + callbacks=callbacks, **kwargs, ) - if parameters_milp.retrieve_all_solution: - n_solutions = min(parameters_milp.n_solutions_max, len(temporaryresults)) - else: - n_solutions = 1 + + def convert_temporaryresults( + self, temporary_results: List[TemporaryResult] + ) -> List[Tuple[Solution, Union[float, TupleFitness]]]: list_solution_fits: List[Tuple[Solution, Union[float, TupleFitness]]] = [] - for s in range(n_solutions): + for temporaryresult in temporary_results: solution = convert_temporaryresult_to_gpdpsolution( - temporaryresult=temporaryresults[s], problem=self.problem + temporaryresult=temporaryresult, problem=self.problem ) fit = self.aggreg_from_sol(solution) list_solution_fits.append((solution, fit)) - return ResultStorage( - list_solution_fits=list_solution_fits, - mode_optim=self.params_objective_function.sense_function, - ) + return list_solution_fits def reapply_constraint( self, list_constraint_tuple: List[Tuple[Set[Tuple[int, int]], int]] diff --git a/discrete_optimization/pickup_vrp/solver/ortools_solver.py b/discrete_optimization/pickup_vrp/solver/ortools_solver.py index 6a9a41e54..c04de2937 100644 --- a/discrete_optimization/pickup_vrp/solver/ortools_solver.py +++ b/discrete_optimization/pickup_vrp/solver/ortools_solver.py @@ -7,12 +7,9 @@ import logging from enum import Enum from functools import partial -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union -import matplotlib.pyplot as plt import numpy as np -from matplotlib.axes import Axes -from matplotlib.figure import Figure from ortools.constraint_solver import ( pywrapcp, routing_enums_pb2, @@ -25,7 +22,6 @@ CallbackList, ) from discrete_optimization.generic_tools.do_problem import ParamsObjectiveFunction -from discrete_optimization.generic_tools.do_solver import SolverDO from discrete_optimization.generic_tools.exceptions import SolveEarlyStop from discrete_optimization.generic_tools.hyperparameters.hyperparameter import ( CategoricalHyperparameter, @@ -1026,40 +1022,3 @@ def convert_to_gpdpsolution( times=times, resource_evolution=resource_evolution, ) - - -def plot_ortools_solution( - sol: GPDPSolution, - problem: GPDP, -) -> Tuple[Figure, Axes]: - if problem.coordinates_2d is None: - raise ValueError( - "problem.coordinates_2d cannot be None when calling plot_ortools_solution." - ) - vehicle_tours = sol.trajectories - fig, ax = plt.subplots(1) - nb_colors = problem.number_vehicle - nb_colors_clusters = len(problem.clusters_set) - colors_nodes = plt.cm.get_cmap("hsv", nb_colors_clusters) - ax.scatter( - [problem.coordinates_2d[node][0] for node in problem.clusters_dict], - [problem.coordinates_2d[node][1] for node in problem.clusters_dict], - s=1, - color=[ - colors_nodes(problem.clusters_dict[node]) for node in problem.clusters_dict - ], - ) - for v, traj in vehicle_tours.items(): - ax.plot( - [problem.coordinates_2d[node][0] for node in traj], - [problem.coordinates_2d[node][1] for node in traj], - label="vehicle n°" + str(v), - ) - ax.scatter( - [problem.coordinates_2d[node][0] for node in traj], - [problem.coordinates_2d[node][1] for node in traj], - s=10, - color=[colors_nodes(problem.clusters_dict[node]) for node in traj], - ) - ax.legend() - return fig, ax diff --git a/examples/pickup_vrp/linear_flow_solver_examples.py b/examples/pickup_vrp/linear_flow_solver_examples.py index 852225631..48014aa03 100644 --- a/examples/pickup_vrp/linear_flow_solver_examples.py +++ b/examples/pickup_vrp/linear_flow_solver_examples.py @@ -5,10 +5,11 @@ from discrete_optimization.pickup_vrp.builders.instance_builders import ( create_ortools_example, ) +from discrete_optimization.pickup_vrp.gpdp import GPDPSolution +from discrete_optimization.pickup_vrp.plots.gpdp_plot_utils import plot_gpdp_solution from discrete_optimization.pickup_vrp.solver.lp_solver import ( LinearFlowSolver, ParametersMilp, - plot_solution, plt, ) @@ -37,10 +38,11 @@ def example_ortools_example(): ) p = ParametersMilp.default() p.time_limit = 100 - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=False, nb_iteration_max=20, include_subtour=False ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) plt.show() diff --git a/examples/pickup_vrp/loading_example.py b/examples/pickup_vrp/loading_example.py index de67338fc..169dadb32 100644 --- a/examples/pickup_vrp/loading_example.py +++ b/examples/pickup_vrp/loading_example.py @@ -19,18 +19,22 @@ create_pickup_and_delivery, create_selective_tsp, ) -from discrete_optimization.pickup_vrp.gpdp import GPDP, ProxyClass, build_pruned_problem +from discrete_optimization.pickup_vrp.gpdp import ( + GPDP, + GPDPSolution, + ProxyClass, + build_pruned_problem, +) +from discrete_optimization.pickup_vrp.plots.gpdp_plot_utils import plot_gpdp_solution from discrete_optimization.pickup_vrp.solver.lp_solver import ( LinearFlowSolver, ParametersMilp, - plot_solution, ) from discrete_optimization.pickup_vrp.solver.ortools_solver import ( FirstSolutionStrategy, LocalSearchMetaheuristic, ORToolsGPDP, ParametersCost, - plot_ortools_solution, ) logging.basicConfig(level=logging.DEBUG) @@ -71,10 +75,11 @@ def debug_lp(): linear_flow_solver = LinearFlowSolver(problem=gpdp) p = ParametersMilp.default() p.time_limit = 2000 - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=False, nb_iteration_max=4, include_subtour=False ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) plt.show() @@ -86,11 +91,11 @@ def selective_tsp(): linear_flow_solver.init_model( one_visit_per_cluster=True, one_visit_per_node=False, include_subtour=False ) - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=True, nb_iteration_max=4, include_subtour=False ) - print(solutions[-1].flow_solution) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) plt.show() @@ -115,10 +120,11 @@ def vrp_capacity(): one_visit_per_node=True, include_time_evolution=False, ) - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=True, nb_iteration_max=4 ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) plt.show() @@ -137,8 +143,7 @@ def run_ortools_solver(): ) result_storage = solver.solve() best_sol = result_storage.best_solution - assert best_sol.check_pickup_deliverable() - plot_ortools_solution(best_sol, gpdp) + plot_gpdp_solution(best_sol, gpdp) plt.show() @@ -166,8 +171,7 @@ def run_ortools_solver_selective(): ) result_storage = solver.solve() best_sol = result_storage.best_solution - assert best_sol.check_pickup_deliverable() - plot_ortools_solution(best_sol, gpdp) + plot_gpdp_solution(best_sol, gpdp) plt.show() @@ -231,7 +235,7 @@ def check_solution(res, gpdp: GPDP): result_storage = solver.solve() best_sol = result_storage.best_solution assert best_sol.check_pickup_deliverable() - plot_ortools_solution(best_sol, model) + plot_gpdp_solution(best_sol, model) plt.show() @@ -256,7 +260,7 @@ def run_ortools_pickup_delivery_cluster(): result_storage = solver.solve() best_sol = result_storage.best_solution assert best_sol.check_pickup_deliverable() - plot_ortools_solution(best_sol, gpdp) + plot_gpdp_solution(best_sol, gpdp) plt.show() diff --git a/examples/pickup_vrp/pickup_vrp_ortools_with_optuna.py b/examples/pickup_vrp/pickup_vrp_ortools_with_optuna.py index 876bed363..02fb6e9d4 100644 --- a/examples/pickup_vrp/pickup_vrp_ortools_with_optuna.py +++ b/examples/pickup_vrp/pickup_vrp_ortools_with_optuna.py @@ -35,12 +35,12 @@ create_selective_tsp, ) from discrete_optimization.pickup_vrp.gpdp import GPDPSolution +from discrete_optimization.pickup_vrp.plots.gpdp_plot_utils import plot_gpdp_solution from discrete_optimization.pickup_vrp.solver.ortools_solver import ( FirstSolutionStrategy, LocalSearchMetaheuristic, ORToolsGPDP, ParametersCost, - plot_ortools_solution, ) SEED = 42 diff --git a/examples/pickup_vrp/pickup_vrp_ortools_with_optuna_auto.py b/examples/pickup_vrp/pickup_vrp_ortools_with_optuna_auto.py index 21e317fea..8d3d80243 100644 --- a/examples/pickup_vrp/pickup_vrp_ortools_with_optuna_auto.py +++ b/examples/pickup_vrp/pickup_vrp_ortools_with_optuna_auto.py @@ -36,12 +36,12 @@ create_selective_tsp, ) from discrete_optimization.pickup_vrp.gpdp import GPDPSolution +from discrete_optimization.pickup_vrp.plots.gpdp_plot_utils import plot_gpdp_solution from discrete_optimization.pickup_vrp.solver.ortools_solver import ( FirstSolutionStrategy, LocalSearchMetaheuristic, ORToolsGPDP, ParametersCost, - plot_ortools_solution, ) SEED = 42 diff --git a/examples/pickup_vrp/time_windows_example.py b/examples/pickup_vrp/time_windows_example.py index 7421e8ea2..3419cb0e0 100644 --- a/examples/pickup_vrp/time_windows_example.py +++ b/examples/pickup_vrp/time_windows_example.py @@ -8,12 +8,12 @@ import matplotlib.pyplot as plt from classic_ortools_example import create_matrix_data +from discrete_optimization.pickup_vrp.plots.gpdp_plot_utils import plot_gpdp_solution from discrete_optimization.pickup_vrp.solver.ortools_solver import ( FirstSolutionStrategy, LocalSearchMetaheuristic, ORToolsGPDP, ParametersCost, - plot_ortools_solution, ) logging.basicConfig(level=logging.DEBUG) @@ -41,8 +41,7 @@ def run_time_windows(): ) result_storage = solver.solve() best_sol = result_storage.best_solution - assert best_sol.check_pickup_deliverable() - plot_ortools_solution(best_sol, gpdp) + plot_gpdp_solution(best_sol, gpdp) plt.show() @@ -69,7 +68,7 @@ def run_pickup(): result_storage = solver.solve() best_sol = result_storage.best_solution assert best_sol.check_pickup_deliverable() - plot_ortools_solution(best_sol, gpdp) + plot_gpdp_solution(best_sol, gpdp) plt.show() @@ -104,8 +103,7 @@ def run_demand(): ) result_storage = solver.solve() best_sol = result_storage.best_solution - assert best_sol.check_pickup_deliverable() - plot_ortools_solution(best_sol, gpdp) + plot_gpdp_solution(best_sol, gpdp) plt.show() diff --git a/tests/pickup_vrp/builders/test_instance_builders.py b/tests/pickup_vrp/builders/test_instance_builders.py index d2286614a..732b68cdf 100644 --- a/tests/pickup_vrp/builders/test_instance_builders.py +++ b/tests/pickup_vrp/builders/test_instance_builders.py @@ -11,12 +11,12 @@ create_selective_tsp, ) from discrete_optimization.pickup_vrp.gpdp import GPDPSolution +from discrete_optimization.pickup_vrp.plots.gpdp_plot_utils import plot_gpdp_solution from discrete_optimization.pickup_vrp.solver.ortools_solver import ( FirstSolutionStrategy, LocalSearchMetaheuristic, ORToolsGPDP, ParametersCost, - plot_ortools_solution, ) @@ -54,7 +54,7 @@ def test_pickup_and_delivery(): best_sol = result_storage.best_solution assert isinstance(best_sol, GPDPSolution) assert best_sol.check_pickup_deliverable() - plot_ortools_solution(best_sol, model) + plot_gpdp_solution(best_sol, model) def test_pickup_and_delivery_equilibrate_new_api(): diff --git a/tests/pickup_vrp/builders/test_linear_flow_solver.py b/tests/pickup_vrp/builders/test_linear_flow_solver.py index d33f9ce1c..df93d38a0 100644 --- a/tests/pickup_vrp/builders/test_linear_flow_solver.py +++ b/tests/pickup_vrp/builders/test_linear_flow_solver.py @@ -3,9 +3,13 @@ # LICENSE file in the root directory of this source tree. import pytest +from matplotlib import pyplot as plt import discrete_optimization.tsp.tsp_parser as tsp_parser import discrete_optimization.vrp.vrp_parser as vrp_parser +from discrete_optimization.generic_tools.callbacks.early_stoppers import ( + NbIterationStopper, +) from discrete_optimization.generic_tools.result_storage.result_storage import ( ResultStorage, ) @@ -17,10 +21,10 @@ ProxyClass, build_pruned_problem, ) +from discrete_optimization.pickup_vrp.plots.gpdp_plot_utils import plot_gpdp_solution from discrete_optimization.pickup_vrp.solver.lp_solver import ( LinearFlowSolver, ParametersMilp, - plot_solution, ) try: @@ -62,6 +66,33 @@ def test_tsp_new_api(): assert nb_nodes_visited == len(gpdp.all_nodes) +@pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") +def test_tsp_cb(): + files_available = tsp_parser.get_data_available() + file_path = [f for f in files_available if "tsp_5_1" in f][0] + tsp_model = tsp_parser.parse_file(file_path) + gpdp = ProxyClass.from_tsp_model_gpdp(tsp_model=tsp_model, compute_graph=True) + linear_flow_solver = LinearFlowSolver(problem=gpdp) + linear_flow_solver.init_model( + one_visit_per_node=True, include_capacity=False, include_time_evolution=False + ) + p = ParametersMilp.default() + + p.time_limit = 100 + iteration_stopper = NbIterationStopper(nb_iteration_max=2) + res = linear_flow_solver.solve( + parameters_milp=p, + do_lns=False, + nb_iteration_max=20, + include_subtour=False, + callbacks=[iteration_stopper], + ) + assert ( + iteration_stopper.nb_iteration > 0 + and iteration_stopper.nb_iteration <= iteration_stopper.nb_iteration_max + ) + + @pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") def test_tsp_new_api_with_time(): files_available = tsp_parser.get_data_available() @@ -109,10 +140,11 @@ def test_tsp(): p = ParametersMilp.default() p.time_limit = 100 - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=False, nb_iteration_max=20, include_subtour=False ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) @pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") @@ -129,10 +161,11 @@ def test_tsp_simplified(): p = ParametersMilp.default() p.time_limit = 100 - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=False, nb_iteration_max=20, include_subtour=False ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) @pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") @@ -148,10 +181,11 @@ def test_vrp(): p = ParametersMilp.default() p.time_limit = 100 - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=False, nb_iteration_max=20, include_subtour=False ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) @pytest.mark.skip(reason="build_pruned_problem() is buggy for now.") @@ -168,10 +202,11 @@ def test_vrp_simplified(): ) p = ParametersMilp.default() p.time_limit = 100 - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=False, nb_iteration_max=20, include_subtour=False ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) @pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") @@ -186,10 +221,11 @@ def test_selective_tsp(): ) p = ParametersMilp.default() p.time_limit = 100 - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=False, nb_iteration_max=20, include_subtour=False ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) @pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") @@ -204,10 +240,11 @@ def test_selective_vrp(): ) p = ParametersMilp.default() p.time_limit = 100 - solutions = linear_flow_solver.solve_iterative( + res = linear_flow_solver.solve_iterative( parameters_milp=p, do_lns=False, nb_iteration_max=20, include_subtour=False ) - plot_solution(solutions[-1], gpdp) + sol: GPDPSolution = res.get_best_solution() + plot_gpdp_solution(sol, gpdp) @pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") diff --git a/tests/pickup_vrp/builders/test_linear_flow_solver_pymip.py b/tests/pickup_vrp/builders/test_linear_flow_solver_pymip.py index ed04d6dc5..31371d01d 100644 --- a/tests/pickup_vrp/builders/test_linear_flow_solver_pymip.py +++ b/tests/pickup_vrp/builders/test_linear_flow_solver_pymip.py @@ -6,6 +6,9 @@ import discrete_optimization.tsp.tsp_parser as tsp_parser import discrete_optimization.vrp.vrp_parser as vrp_parser +from discrete_optimization.generic_tools.callbacks.early_stoppers import ( + NbIterationStopper, +) from discrete_optimization.generic_tools.result_storage.result_storage import ( ResultStorage, ) @@ -17,7 +20,6 @@ ProxyClass, build_pruned_problem, ) -from discrete_optimization.pickup_vrp.solver.lp_solver import plot_solution from discrete_optimization.pickup_vrp.solver.lp_solver_pymip import ( LinearFlowSolver, ParametersMilp, @@ -67,6 +69,28 @@ def test_tsp(): assert nb_nodes_visited == len(gpdp.all_nodes) +def test_tsp_cb(): + files_available = tsp_parser.get_data_available() + file_path = [f for f in files_available if "tsp_5_1" in f][0] + tsp_model = tsp_parser.parse_file(file_path) + gpdp = ProxyClass.from_tsp_model_gpdp(tsp_model=tsp_model, compute_graph=True) + linear_flow_solver = LinearFlowSolver(problem=gpdp) + linear_flow_solver.init_model( + one_visit_per_node=True, + include_capacity=False, + include_time_evolution=False, + ) + p = ParametersMilp.default() + iteration_stopper = NbIterationStopper(nb_iteration_max=2) + res = linear_flow_solver.solve( + parameters_milp=p, + do_lns=True, + nb_iteration_max=20, + callbacks=[iteration_stopper], + ) + assert iteration_stopper.nb_iteration == iteration_stopper.nb_iteration_max + + def test_tsp_with_time(): files_available = tsp_parser.get_data_available() file_path = [f for f in files_available if "tsp_5_1" in f][0]