Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement callbacks for iterative pickup-vrp milp solvers #183

Merged
merged 4 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
50 changes: 50 additions & 0 deletions discrete_optimization/pickup_vrp/plots/gpdp_plot_utils.py
Original file line number Diff line number Diff line change
@@ -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
148 changes: 66 additions & 82 deletions discrete_optimization/pickup_vrp/solver/lp_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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(
[
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
)
Loading