From da921171e9fc8df1e412d1d0815e43844a346f3a Mon Sep 17 00:00:00 2001
From: Helldragon67 <kilian.scheidecker@orange.fr>
Date: Mon, 12 Aug 2024 15:58:30 +0200
Subject: [PATCH] more balanced scores

---
 .../src/parameters/landmark_parameters.yaml   | 15 ++--
 .../src/parameters/optimizer_parameters.yaml  |  3 +-
 backend/src/structs/linked_landmarks.py       | 21 ++++++
 backend/src/tester.py                         | 20 +++---
 backend/src/utils/landmarks_manager.py        | 71 +++++++++++--------
 backend/src/utils/optimizer.py                | 10 ++-
 backend/src/utils/refiner.py                  | 11 +--
 7 files changed, 97 insertions(+), 54 deletions(-)

diff --git a/backend/src/parameters/landmark_parameters.yaml b/backend/src/parameters/landmark_parameters.yaml
index 777c18f..983b9c8 100644
--- a/backend/src/parameters/landmark_parameters.yaml
+++ b/backend/src/parameters/landmark_parameters.yaml
@@ -1,6 +1,11 @@
-city_bbox_side: 5000 #m
+city_bbox_side: 7500 #m
 radius_close_to: 50
-church_coeff: 0.8
-park_coeff: 1.0
-tag_coeff: 10
-N_important: 40
+church_coeff: 0.75
+nature_coeff: 1.25
+overall_coeff: 10
+tag_exponent: 1.15
+image_bonus: 10
+viewpoint_bonus: 15
+wikipedia_bonus: 6
+N_important: 50
+pay_bonus: -1
diff --git a/backend/src/parameters/optimizer_parameters.yaml b/backend/src/parameters/optimizer_parameters.yaml
index 2bf4f32..6dc49d2 100644
--- a/backend/src/parameters/optimizer_parameters.yaml
+++ b/backend/src/parameters/optimizer_parameters.yaml
@@ -1,4 +1,5 @@
 detour_factor: 1.4
 detour_corridor_width: 200
 average_walking_speed: 4.8
-max_landmarks: 7
+max_landmarks: 10
+max_landmarks_refiner: 20
diff --git a/backend/src/structs/linked_landmarks.py b/backend/src/structs/linked_landmarks.py
index 7d350f5..6c74c74 100644
--- a/backend/src/structs/linked_landmarks.py
+++ b/backend/src/structs/linked_landmarks.py
@@ -25,6 +25,11 @@ class LinkedLandmarks:
         """
         Create the links between the landmarks in the list by setting their .next_uuid and the .time_to_next attributes.
         """
+
+        # Mark secondary landmarks as such
+        self.update_secondary_landmarks()
+
+
         for i, landmark in enumerate(self._landmarks[:-1]):
             landmark.next_uuid = self._landmarks[i + 1].uuid
             time_to_next = get_time(landmark.location, self._landmarks[i + 1].location)
@@ -34,6 +39,22 @@ class LinkedLandmarks:
         self._landmarks[-1].next_uuid = None
         self._landmarks[-1].time_to_reach_next = 0
 
+    def update_secondary_landmarks(self) -> None:
+        # Extract the attractiveness scores and sort them in descending order
+        scores = sorted([landmark.attractiveness for landmark in self._landmarks], reverse=True)
+        
+        # Determine the 10th highest score
+        if len(scores) >= 10:
+            threshold_score = scores[9]
+        else:
+            # If there are fewer than 10 landmarks, use the lowest score in the list as the threshold
+            threshold_score = min(scores) if scores else 0
+        
+        # Update 'is_secondary' for landmarks with attractiveness below the threshold score
+        for landmark in self._landmarks:
+            if landmark.attractiveness < threshold_score:
+                landmark.is_secondary = True
+
 
     def __getitem__(self, index: int) -> Landmark:
         return self._landmarks[index]
diff --git a/backend/src/tester.py b/backend/src/tester.py
index f83c4a7..acfec75 100644
--- a/backend/src/tester.py
+++ b/backend/src/tester.py
@@ -24,8 +24,8 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
         nature=Preference(type='nature', score = 5),
         shopping=Preference(type='shopping', score = 5),
 
-        max_time_minute=180,
-        detour_tolerance_minute=30
+        max_time_minute=1000,
+        detour_tolerance_minute=0
     )
 
     # Create start and finish 
@@ -60,7 +60,9 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
     refined_tour = refiner.refine_optimization(all_landmarks=landmarks, base_tour=base_tour, max_time = preferences.max_time_minute, detour = preferences.detour_tolerance_minute)
 
     linked_tour = LinkedLandmarks(refined_tour)
-    logger.info(f"Optimized route: {linked_tour}")
+    logger.info("Optimized route : ")
+    for l in linked_tour :
+        logger.info(f"{l}")
 
     # with open('linked_tour.yaml', 'w') as f:
     #     yaml.dump(linked_tour.asdict(), f)
@@ -68,9 +70,9 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] =
     return linked_tour
 
 
-#test(tuple((48.8344400, 2.3220540)))       # Café Chez César 
-#test(tuple((48.8375946, 2.2949904)))       # Point random
-#test(tuple((47.377859, 8.540585)))         # Zurich HB
-#test(tuple((45.7576485, 4.8330241)))      # Lyon Bellecour
-test(tuple((48.5848435, 7.7332974)))      # Strasbourg Gare
-#test(tuple((48.2067858, 16.3692340)))      # Vienne
+test(tuple((48.8344400, 2.3220540)))       # Café Chez César 
+# test(tuple((48.8375946, 2.2949904)))       # Point random
+# test(tuple((47.377859, 8.540585)))         # Zurich HB
+# test(tuple((45.7576485, 4.8330241)))      # Lyon Bellecour
+# test(tuple((48.5848435, 7.7332974)))      # Strasbourg Gare
+# test(tuple((48.2067858, 16.3692340)))      # Vienne
diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py
index 6cde21c..67fb086 100644
--- a/backend/src/utils/landmarks_manager.py
+++ b/backend/src/utils/landmarks_manager.py
@@ -23,8 +23,8 @@ class LandmarkManager:
 
     radius_close_to: int    # radius in meters
     church_coeff: float     # coeff to adjsut score of churches
-    park_coeff: float       # coeff to adjust score of parks
-    tag_coeff: float        # coeff to adjust weight of tags
+    nature_coeff: float       # coeff to adjust score of parks
+    overall_coeff: float        # coeff to adjust weight of tags
     N_important: int        # number of important landmarks to consider
 
 
@@ -38,8 +38,13 @@ class LandmarkManager:
             self.max_bbox_side = parameters['city_bbox_side']
             self.radius_close_to = parameters['radius_close_to']
             self.church_coeff = parameters['church_coeff']
-            self.park_coeff = parameters['park_coeff']
-            self.tag_coeff = parameters['tag_coeff']
+            self.nature_coeff = parameters['nature_coeff']
+            self.overall_coeff = parameters['overall_coeff']
+            self.tag_exponent = parameters['tag_exponent']
+            self.image_bonus = parameters['image_bonus']
+            self.wikipedia_bonus = parameters['wikipedia_bonus']
+            self.viewpoint_bonus = parameters['viewpoint_bonus']
+            self.pay_bonus = parameters['pay_bonus']
             self.N_important = parameters['N_important']
             
         with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
@@ -76,25 +81,25 @@ class LandmarkManager:
         bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
         # list for sightseeing
         if preferences.sightseeing.score != 0:
-            score_function = lambda loc, n_tags: int((((n_tags**1.2)*self.tag_coeff) )*self.church_coeff)   # self.count_elements_close_to(loc) +
+            score_function = lambda score: int(score*10)*preferences.sightseeing.score/5   # self.count_elements_close_to(loc) +
             L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
             L += L1
 
         # list for nature
         if preferences.nature.score != 0:
-            score_function = lambda loc, n_tags: int((((n_tags**1.2)*self.tag_coeff) )*self.park_coeff)   # self.count_elements_close_to(loc) +
+            score_function = lambda score: int(score*10*self.nature_coeff)*preferences.nature.score/5   # self.count_elements_close_to(loc) +
             L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
             L += L2
 
         # list for shopping
         if preferences.shopping.score != 0:
-            score_function = lambda loc, n_tags: int(((n_tags**1.2)*self.tag_coeff)) # self.count_elements_close_to(loc) +
+            score_function = lambda score: int(score*10)*preferences.shopping.score/5 # self.count_elements_close_to(loc) +
             L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
             L += L3
 
 
         L = self.remove_duplicates(L)
-        self.correct_score(L, preferences)
+        # self.correct_score(L, preferences)
 
         L_constrained = take_most_important(L, self.N_important)
         self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.')
@@ -273,8 +278,8 @@ class LandmarkManager:
                     continue
 
                 # skip if unused
-                if 'disused:leisure' in elem.tags().keys():
-                    continue
+                # if 'disused:leisure' in elem.tags().keys():
+                #     continue
                 
                 # skip if part of another building
                 if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
@@ -284,19 +289,20 @@ class LandmarkManager:
                 osm_id = elem.id()                  # Add OSM id 
                 elem_type = landmarktype            # Add the landmark type as 'sightseeing, 
                 n_tags = len(elem.tags().keys())    # Add number of tags
+                score = n_tags**self.tag_exponent   # Add score
 
                 # remove specific tags
                 skip = False
                 for tag in elem.tags().keys():
                     if "pay" in tag:
-                        n_tags -= 1             # discard payment options for tags
+                        score += self.pay_bonus             # discard payment options for tags
 
                     if "disused" in tag:
                         skip = True             # skip disused amenities
                         break
 
-                    if "wikipedia" in tag:
-                        n_tags += 1             # wikipedia entries count more
+                    if "wiki" in tag:
+                        score += self.wikipedia_bonus             # wikipedia entries count more
 
                     # if tag == "wikidata":
                     #     Q = elem.tag('wikidata')
@@ -305,15 +311,16 @@ class LandmarkManager:
                     #     item.get()
                     #     n_languages = len(item.labels)
                     #     n_tags += n_languages/10
+
                     if "viewpoint" in tag:
-                        n_tags += 10
+                        score += self.viewpoint_bonus
+
+                    if "image" in tag:
+                        score += self.image_bonus
 
                     if elem_type != "nature":
                         if "leisure" in tag and elem.tag('leisure') == "park":
                             elem_type = "nature"
-                    
-                    if elem_type == "nature":
-                        n_tags += 1 
 
                     if landmarktype != "shopping":
                         if "shop" in tag:
@@ -326,20 +333,22 @@ class LandmarkManager:
                 if skip:
                     continue
 
-                score = score_function(location, n_tags)
-                if score != 0:
-                    # Generate the landmark and append it to the list
-                    landmark = Landmark(
-                        name=name,
-                        type=elem_type,
-                        location=location,
-                        osm_type=osm_type,
-                        osm_id=osm_id,
-                        attractiveness=score,
-                        must_do=False,
-                        n_tags=int(n_tags)
-                    )
-                    return_list.append(landmark)
+                score = score_function(score)
+                if "place_of_worship" in elem.tags().values() :
+                    score = int(score*self.church_coeff)
+
+                # Generate the landmark and append it to the list
+                landmark = Landmark(
+                    name=name,
+                    type=elem_type,
+                    location=location,
+                    osm_type=osm_type,
+                    osm_id=osm_id,
+                    attractiveness=score,
+                    must_do=False,
+                    n_tags=int(n_tags)
+                )
+                return_list.append(landmark)
         
         self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")
 
diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py
index 931a3d2..947c0dc 100644
--- a/backend/src/utils/optimizer.py
+++ b/backend/src/utils/optimizer.py
@@ -203,7 +203,7 @@ class Optimizer:
         return c, A_ub, [max_steps]
 
 
-    def respect_number(self, L: int):
+    def respect_number(self, L, max_landmarks: int):
         """
         Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks.
 
@@ -224,7 +224,7 @@ class Optimizer:
             b.append(1)
 
         A = np.vstack((A, ones*L))
-        b.append(self.max_landmarks+1)
+        b.append(max_landmarks+1)
 
         return A, b
 
@@ -433,6 +433,7 @@ class Optimizer:
             self,
             max_time: int,
             landmarks: list[Landmark],
+            max_landmarks: int = None
         ) -> list[Landmark]:
         """
         Main optimization pipeline to solve the landmark visiting problem.
@@ -443,15 +444,18 @@ class Optimizer:
         Args:
             max_time (int): Maximum time allowed for the tour in minutes.
             landmarks (list[Landmark]): List of landmarks to visit.
+            max_landmarks (int): Maximum number of landmarks visited
         Returns:
             list[Landmark]: The optimized tour of landmarks with updated travel times, or None if no valid solution is found.
         """
+        if max_landmarks is None :
+            max_landmarks = self.max_landmarks
 
         L = len(landmarks)
 
         # SET CONSTRAINTS FOR INEQUALITY
         c, A_ub, b_ub = self.init_ub_dist(landmarks, max_time)          # Add the distances from each landmark to the other
-        A, b = self.respect_number(L)                     # Respect max number of visits (no more possible stops than landmarks). 
+        A, b = self.respect_number(L, max_landmarks)                                   # Respect max number of visits (no more possible stops than landmarks). 
         A_ub = np.vstack((A_ub, A), dtype=np.int16)
         b_ub += b
         A, b = self.break_sym(L)                                         # break the 'zig-zag' symmetry
diff --git a/backend/src/utils/refiner.py b/backend/src/utils/refiner.py
index c8f0aa5..871e332 100644
--- a/backend/src/utils/refiner.py
+++ b/backend/src/utils/refiner.py
@@ -17,7 +17,7 @@ class Refiner :
     detour_factor: float            # detour factor of straight line vs real distance in cities
     detour_corridor_width: float    # width of the corridor around the path
     average_walking_speed: float    # average walking speed of adult
-    max_landmarks: int              # max number of landmarks to visit
+    max_landmarks_refiner: int              # max number of landmarks to visit
     optimizer: Optimizer            # optimizer object
 
     def __init__(self, optimizer: Optimizer) :
@@ -29,7 +29,7 @@ class Refiner :
             self.detour_factor = parameters['detour_factor']
             self.detour_corridor_width = parameters['detour_corridor_width']
             self.average_walking_speed = parameters['average_walking_speed']
-            self.max_landmarks = parameters['max_landmarks'] + 4
+            self.max_landmarks_refiner = parameters['max_landmarks_refiner']
 
 
     def create_corridor(self, landmarks: list[Landmark], width: float) :
@@ -308,8 +308,8 @@ class Refiner :
         """
 
         # No need to refine if no detour is taken
-        if detour == 0:
-            return base_tour
+        # if detour == 0:
+        #     return base_tour
         
         minor_landmarks = self.get_minor_landmarks(all_landmarks, base_tour, self.detour_corridor_width)
 
@@ -322,7 +322,8 @@ class Refiner :
         # get a new tour
         new_tour = self.optimizer.solve_optimization(
             max_time = max_time + detour,
-            landmarks = full_set
+            landmarks = full_set, 
+            max_landmarks = self.max_landmarks_refiner
         )
 
         if new_tour is None: