Coverage for o2/agents/tabu_agent.py: 100%
38 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-16 11:18 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-16 11:18 +0000
1import math
2from typing import Optional
4from typing_extensions import override
6from o2.agents.agent import (
7 Agent,
8 NoNewBaseSolutionFoundError,
9)
10from o2.models.solution import Solution
11from o2.store import SolutionTry
12from o2.util.indented_printer import print_l2, print_l3
15class TabuAgent(Agent):
16 """Selects the best action to take next, based on the current state of the store.
18 This Action Selector is based on the Tabu Search algorithm, and also has options to
19 support the simulated annealing.
20 """
22 @override
23 def find_new_base_solution(self, proposed_solution_try: Optional[SolutionTry] = None) -> Solution:
24 solution = self._select_new_base_evaluation(
25 # If the proposed solution try is None, we were called from
26 # maximum non improving iterations so we don't need to reinsert
27 # the current solution
28 reinsert_current_solution=proposed_solution_try is not None,
29 )
30 if proposed_solution_try is not None and proposed_solution_try[1].id == solution.id:
31 print_l3("The proposed solution is the same as the newly found solution (see below).")
32 return solution
34 @override
35 def process_many_solutions(
36 self, solutions: list[Solution]
37 ) -> tuple[list[SolutionTry], list[SolutionTry]]:
38 chosen_tries, not_chosen_tries = super().process_many_solutions(solutions)
40 solutions_in_radius = 0
41 for _, solution_try in not_chosen_tries:
42 min_distance = min(
43 solution_try.distance_to(solution) for solution in self.store.current_pareto_front.solutions
44 )
45 if solution_try.is_valid and min_distance <= self.get_max_distance():
46 solutions_in_radius += 1
47 print_l2(
48 f"Out of the {len(not_chosen_tries)} not chosen tries, {solutions_in_radius} are in the radius."
49 )
50 return chosen_tries, not_chosen_tries
52 def get_max_distance(self) -> float:
53 """Get the maximum distance to a new base solution, aka the error radius."""
54 if self.store.settings.max_distance_to_new_base_solution != float("inf"):
55 return self.store.settings.max_distance_to_new_base_solution
56 elif self.store.settings.error_radius_in_percent is not None:
57 return self.store.settings.error_radius_in_percent * math.sqrt(
58 self.store.base_evaluation.pareto_x**2 + self.store.base_evaluation.pareto_y**2
59 )
60 else:
61 return float("inf")
63 def _select_new_base_evaluation(self, reinsert_current_solution: bool = False) -> Solution:
64 """Choose a new base evaluation from the solution tree.
66 If reinsert_current_solution is True, the current solution will be
67 reinserted into the solution tree. This is useful if you aren't
68 sure if you exhausted all possible actions for this solution.
69 """
70 if reinsert_current_solution:
71 self.store.solution_tree.add_solution(self.store.solution, archive=False)
73 max_distance = self.get_max_distance()
75 new_solution = self.store.solution_tree.pop_nearest_solution(
76 self.store.current_pareto_front, max_distance=max_distance
77 )
79 if new_solution is None:
80 raise NoNewBaseSolutionFoundError()
82 return new_solution