Skip to content

Commit

Permalink
feat: improved stats for KGLS
Browse files Browse the repository at this point in the history
  • Loading branch information
ArnoldF committed Jan 8, 2025
1 parent e410108 commit 4cceeb4
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 42 deletions.
8 changes: 5 additions & 3 deletions examples/complex_run/main.py → examples/custom_run/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
35 changes: 25 additions & 10 deletions examples/run_benchmark/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"""
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)
"""
6 changes: 5 additions & 1 deletion kgls/datastructure/vrp_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
86 changes: 62 additions & 24 deletions kgls/kgls.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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__
Expand All @@ -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,
Expand All @@ -92,21 +87,28 @@ 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()
self._best_solution_costs = current_costs
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):
Expand Down Expand Up @@ -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.'
Expand All @@ -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)
4 changes: 0 additions & 4 deletions kgls/local_search/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

0 comments on commit 4cceeb4

Please sign in to comment.