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: