From 4cceeb43bfdaf46f4bb031dc8399ecbaa3193484 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 8 Jan 2025 11:46:49 +0100 Subject: [PATCH] feat: improved stats for KGLS --- .../instances/X-n101-k25.sol | 0 .../instances/X-n101-k25.vrp | 0 examples/{complex_run => custom_run}/main.py | 8 +- examples/run_benchmark/main.py | 35 +++++--- kgls/datastructure/vrp_solution.py | 6 +- kgls/kgls.py | 86 +++++++++++++------ kgls/local_search/search.py | 4 - 7 files changed, 97 insertions(+), 42 deletions(-) rename examples/{complex_run => custom_run}/instances/X-n101-k25.sol (100%) rename examples/{complex_run => custom_run}/instances/X-n101-k25.vrp (100%) rename examples/{complex_run => custom_run}/main.py (92%) diff --git a/examples/complex_run/instances/X-n101-k25.sol b/examples/custom_run/instances/X-n101-k25.sol similarity index 100% rename from examples/complex_run/instances/X-n101-k25.sol rename to examples/custom_run/instances/X-n101-k25.sol diff --git a/examples/complex_run/instances/X-n101-k25.vrp b/examples/custom_run/instances/X-n101-k25.vrp similarity index 100% rename from examples/complex_run/instances/X-n101-k25.vrp rename to examples/custom_run/instances/X-n101-k25.vrp diff --git a/examples/complex_run/main.py b/examples/custom_run/main.py similarity index 92% rename from examples/complex_run/main.py rename to examples/custom_run/main.py index 371bafb..6bc6777 100644 --- a/examples/complex_run/main.py +++ b/examples/custom_run/main.py @@ -25,9 +25,10 @@ neighborhood_size=10, moves=['segment_move', 'relocation_chain'] ) -kgls_light.set_abortion_condition("runtime_without_improvement", 10) +kgls_light.set_abortion_condition("runtime_without_improvement", 1) kgls_light.run(visualize_progress=False) -kgls_light.print_stats() + +kgls_light.print_time_distribution() kgls_light.best_solution_to_file('interim_solution.txt') # continue from above solution with a longer search and 'more heavy' parameters @@ -41,4 +42,5 @@ ) kgls_heavy.set_abortion_condition("runtime_without_improvement", 120) kgls_heavy.start_from_solution('interim_solution.txt', False) -kgls_heavy.print_stats() + +kgls_light.best_solution_to_file('final_solution.txt') diff --git a/examples/run_benchmark/main.py b/examples/run_benchmark/main.py index 66c8e53..9167c60 100644 --- a/examples/run_benchmark/main.py +++ b/examples/run_benchmark/main.py @@ -10,22 +10,37 @@ instance_path = os.path.join(Path(__file__).resolve().parent, 'instances') -all_instances = sorted([f for f in os.listdir(instance_path) if f.endswith('.vrp')])[50:60] +all_instances = sorted([f for f in os.listdir(instance_path) if f.endswith('.vrp')])[1:35] + +gaps = dict() +run_times = dict() for file in all_instances: logger.info(f'Solving {file}') file_path = os.path.join(instance_path, file) kgls = KGLS(file_path) - #kgls.set_abortion_condition("iterations_without_improvement", 100) kgls.set_abortion_condition("runtime_without_improvement", 120) kgls.run(visualize_progress=False) - """ - import cProfile - import pstats - cProfile.run('kgls.run(visualize_progress=False)')#, 'output.prof') - with open("output.prof") as f: - p = pstats.Stats(f) - p.sort_stats("cumulative").print_stats(10) - """ \ No newline at end of file + gaps[file] = kgls.best_found_gap + run_times[file] = kgls.total_runtime + +logger.info(f'Benchmark summary') +logger.info(f'Average gap: {sum(gaps.values()) / len(gaps):.2f}') +logger.info(f'Average run_time: {sum(run_times.values()) / len(run_times):.0f}') +logger.info(f'Detailed Results') +logger.info(f"{'Instance':<20}{'Time':<5}{'Gap':<5}") +logger.info("-" * 30) +for instance in gaps.keys(): + logger.info(f"{instance:<20}{int(run_times[instance]):<5}{gaps[instance]:.2f}") + + +""" +import cProfile +import pstats +cProfile.run('kgls.run(visualize_progress=False)')#, 'output.prof') +with open("output.prof") as f: + p = pstats.Stats(f) +p.sort_stats("cumulative").print_stats(10) +""" \ No newline at end of file diff --git a/kgls/datastructure/vrp_solution.py b/kgls/datastructure/vrp_solution.py index 1fea677..08e0e77 100644 --- a/kgls/datastructure/vrp_solution.py +++ b/kgls/datastructure/vrp_solution.py @@ -220,7 +220,11 @@ def plot(self, solution_value: float): self._plotted_edges[route_index].set_data(x_coordinates, y_coordinates) # update value chart - current_gap = 100 * (solution_value - self.problem.bks) / self.problem.bks + if self.problem.bks != float('inf'): + current_gap = 100 * (solution_value - self.problem.bks) / self.problem.bks + else: + current_gap = solution_value + self._time_steps.append(len(self._time_steps) + 1) self._solution_values.append(current_gap) self._chart_line.set_data(self._time_steps, self._solution_values) diff --git a/kgls/kgls.py b/kgls/kgls.py index 707254f..405d3c8 100644 --- a/kgls/kgls.py +++ b/kgls/kgls.py @@ -1,7 +1,7 @@ import logging import math import time -from typing import Any +from typing import Any, Optional from .datastructure import CostEvaluator, VRPProblem, VRPSolution from .read_write.problem_reader import read_vrp_instance @@ -22,12 +22,12 @@ } -class KGLS(): +class KGLS: _abortion_condition: BaseAbortionCondition _vrp_instance: VRPProblem _cost_evaluator: CostEvaluator - _best_solution: VRPSolution - _cur_solution: VRPSolution + _best_solution: Optional[VRPSolution] + _cur_solution: Optional[VRPSolution] _iteration: int _best_solution_costs: int _best_iteration: int @@ -43,11 +43,14 @@ def __init__(self, path_to_instance_file: str, **kwargs): self._best_solution = None self._abortion_condition = IterationsWithoutImprovementCondition(100) - def _get_run_parameters(self, **kwargs) -> dict[str, Any]: + @staticmethod + def _get_run_parameters(**kwargs) -> dict[str, Any]: # Check user-provided parameters for key, value in kwargs.items(): if key not in DEFAULT_PARAMETERS: - raise ValueError(f"Invalid parameter: {key}") + raise ValueError( + f'Invalid parameter: {key}. ' + f'Parameter must be in {", ".join(DEFAULT_PARAMETERS.keys())}') if key != 'moves' and not isinstance(value, int): actual_type = type(value).__name__ @@ -64,19 +67,11 @@ def _get_run_parameters(self, **kwargs) -> dict[str, Any]: params = {**DEFAULT_PARAMETERS, **kwargs} return params - @property - def best_solution(self): - return self._best_solution - def best_solution_to_file(self, path_to_file: str): self._best_solution.to_file(path_to_file) def set_abortion_condition(self, condition_name: str, param: int): - """ - Set the abortion condition for KGLS. - :param condition_name: Name of the condition ('iterations_with_improvement', 'max_iterations', 'max_runtime'). - :param params: Parameter for the condition. - """ + # Set the abortion condition for KGLS. condition_classes = { "max_iterations": MaxIterationsCondition, "max_runtime": MaxRuntimeCondition, @@ -92,10 +87,16 @@ def set_abortion_condition(self, condition_name: str, param: int): def _update_run_stats(self, start_time): current_costs = self._cost_evaluator.get_solution_costs(self._cur_solution) + if self._vrp_instance.bks != float('inf'): + solution_quality = 100 * (current_costs - self._vrp_instance.bks) / self._vrp_instance.bks + else: + solution_quality = current_costs + + # update stats if new best solution was found if current_costs < self._best_solution_costs: logger.info( - f'{(time.time() - start_time): 1f} ' \ - f'{100 * (current_costs - self._vrp_instance.bks) / self._vrp_instance.bks: .2f}' + f'{(time.time() - start_time): 1f} ' + f'{solution_quality: .2f}' ) self._best_iteration = self._iteration self._best_solution_time = time.time() @@ -103,10 +104,11 @@ def _update_run_stats(self, start_time): self._best_solution = self._cur_solution.copy() self._run_stats.append({ - "time": time.time(), + "run_time": time.time() - start_time, "iteration": self._iteration, "costs": current_costs, "best_costs": self._best_solution_costs, + "best_gap": None if self._vrp_instance.bks == float('inf') else solution_quality, }) def run(self, visualize_progress: bool = False, start_solution: VRPSolution = None): @@ -163,12 +165,47 @@ def run(self, visualize_progress: bool = False, start_solution: VRPSolution = No logger.info(f'KLGS finished after {(time.time() - start_time): 1f} seconds and ' f'{self._iteration} iterations.') - def print_stats(self): - for key in sorted(self._cur_solution.solution_stats.keys()): - print(f"{key}:\t{int(self._cur_solution.solution_stats[key])}") + def print_time_distribution(self): + time_entries = { + k.replace("time_", ""): v + for k, v in self._cur_solution.solution_stats.items() + if k.startswith("time_") + } + + # Print table header + print(f"{'Move':<20}{'Time Percentage':<15}") + print("-" * 35) + + # Print rows + running_percentage = 0 + for key, run_time in time_entries.items(): + percentage = (run_time / self.total_runtime) * 100 + running_percentage += percentage + print(f"{key:<20}{int(percentage):<15}") + + print(f"{'Other':<20}{int(100 - running_percentage):<15}%") + + @property + def best_solution(self): + return self._best_solution + + @property + def best_found_solution_value(self) -> int: + return self._best_solution_costs + + @property + def best_found_gap(self) -> Optional[float]: + if self._vrp_instance.bks != float('inf'): + return 100 * (self._best_solution_costs - self._vrp_instance.bks) / self._vrp_instance.bks + else: + return None + + @property + def total_runtime(self): + return self._run_stats[-1]["run_time"] def _load_solution(self, path_to_file: str) -> VRPSolution: - if not self._cur_solution is None: + if self._cur_solution is not None: raise ValueError( 'Cannot overwrite current solution with a new one. ' 'Please create a new KGLS instance to start from a solution.' @@ -178,8 +215,9 @@ def _load_solution(self, path_to_file: str) -> VRPSolution: return solution def start_from_solution(self, path_to_file: str, visualize_progress: bool = False): + logger.info(f'Continuing KGLS from specified solution') + logger.info(f'Loading starting solution') starting_solution = self._load_solution(path_to_file) - logger.info(f'Continuing KGLS from specified solution') - self.run(visualize_progress, starting_solution) + self.run(visualize_progress=visualize_progress, start_solution=starting_solution) diff --git a/kgls/local_search/search.py b/kgls/local_search/search.py index 3645449..3145cdb 100644 --- a/kgls/local_search/search.py +++ b/kgls/local_search/search.py @@ -180,7 +180,6 @@ def perturbate_solution( cost_evaluator: CostEvaluator, run_parameters: dict[str, Any], ) -> set[Route]: - start = time.time() logger.debug('Starting perturbation of solution') # add previous penalties to costs of edges and compute the badness of the edges of the current solution @@ -202,7 +201,4 @@ def perturbate_solution( cost_evaluator.disable_penalization() - end = time.time() - solution.solution_stats['time_perturbation'] += end - start - return changed_routes_perturbation