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

1import math 

2from typing import Optional 

3 

4from typing_extensions import override 

5 

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 

13 

14 

15class TabuAgent(Agent): 

16 """Selects the best action to take next, based on the current state of the store. 

17 

18 This Action Selector is based on the Tabu Search algorithm, and also has options to 

19 support the simulated annealing. 

20 """ 

21 

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 

33 

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) 

39 

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 

51 

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") 

62 

63 def _select_new_base_evaluation(self, reinsert_current_solution: bool = False) -> Solution: 

64 """Choose a new base evaluation from the solution tree. 

65 

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) 

72 

73 max_distance = self.get_max_distance() 

74 

75 new_solution = self.store.solution_tree.pop_nearest_solution( 

76 self.store.current_pareto_front, max_distance=max_distance 

77 ) 

78 

79 if new_solution is None: 

80 raise NoNewBaseSolutionFoundError() 

81 

82 return new_solution