fixed optimizer. works fine now
This commit is contained in:
		@@ -1,46 +1,80 @@
 | 
			
		||||
import math as m
 | 
			
		||||
 | 
			
		||||
from OSMPythonTools.api import Api
 | 
			
		||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder, Nominatim
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from pydantic import BaseModel
 | 
			
		||||
import math as m
 | 
			
		||||
from structs.landmarks import Landmark, LandmarkType
 | 
			
		||||
from structs.preferences import Preferences, Preference
 | 
			
		||||
from typing import List
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
 | 
			
		||||
RADIUS = 0.0005             # size of the bbox in degrees. 0.0005 ~ 50m
 | 
			
		||||
BBOX_SIDE = 10              # size of bbox in km for general area, 10km
 | 
			
		||||
RADIUS_CLOSE_TO = 50       # size of area in m for close features, 5àm radius
 | 
			
		||||
MIN_SCORE = 100             # discard elements with score < 100
 | 
			
		||||
 | 
			
		||||
BBOX_SIDE = 10              # size of bbox in *km* for general area, 10km
 | 
			
		||||
RADIUS_CLOSE_TO = 25        # size of area in *m* for close features, 5àm radius
 | 
			
		||||
MIN_SCORE = 30             # discard elements with score < 100
 | 
			
		||||
MIN_TAGS = 5                # discard elements withs less than 5 tags
 | 
			
		||||
 | 
			
		||||
SIGHTSEEING = LandmarkType(landmark_type='sightseeing')
 | 
			
		||||
NATURE = LandmarkType(landmark_type='nature')
 | 
			
		||||
SHOPPING = LandmarkType(landmark_type='shopping')
 | 
			
		||||
 | 
			
		||||
# Include th json here
 | 
			
		||||
 | 
			
		||||
# Include the json here
 | 
			
		||||
# Create a list of all things to visit given some preferences and a city. Ready for the optimizer
 | 
			
		||||
def generate_landmarks(coordinates: Tuple[float, float], preferences: Preferences) :
 | 
			
		||||
def generate_landmarks(preferences: Preferences, city_country: str = None, coordinates: Tuple[float, float] = None)->Tuple[List[Landmark], List[Landmark]] :
 | 
			
		||||
 | 
			
		||||
    l_sights = ["'tourism'='museum'", "'tourism'='attraction'", "'tourism'='gallery'", 'historic', "'amenity'='arts_centre'", "'amenity'='planetarium'", "'amenity'='place_of_worship'", "'amenity'='fountain'", '"water"="reflecting_pool"'] 
 | 
			
		||||
    l_nature = ["'leisure'='park'", 'geological', "'natural'='geyser'", "'natural'='hot_spring'", '"natural"="arch"', '"natural"="cave_entrance"', '"natural"="volcano"', '"natural"="stone"', '"tourism"="alpine_hut"', '"tourism"="picnic_site"', '"tourism"="viewpoint"', '"tourism"="zoo"', '"waterway"="waterfall"'] 
 | 
			
		||||
    l_shop = ["'shop'='department_store'", "'shop'='mall'"] #, '"shop"="collector"', '"shop"="antiques"'] 
 | 
			
		||||
  
 | 
			
		||||
    L = []
 | 
			
		||||
 | 
			
		||||
    # Use 'City, Country'
 | 
			
		||||
    if city_country is not None :
 | 
			
		||||
 | 
			
		||||
        # List for sightseeing
 | 
			
		||||
    L1 = get_landmarks(coordinates, l_sights, LandmarkType(landmark_type='sightseeing'))
 | 
			
		||||
        if preferences.sightseeing.score != 0 :
 | 
			
		||||
            L1 = get_landmarks_nominatim(city_country, l_sights, SIGHTSEEING)
 | 
			
		||||
            correct_score(L1, preferences.sightseeing)
 | 
			
		||||
            L += L1
 | 
			
		||||
        
 | 
			
		||||
        # List for nature
 | 
			
		||||
    L2 = get_landmarks(coordinates, l_nature, LandmarkType(landmark_type='nature'))
 | 
			
		||||
        if preferences.nature.score != 0 :
 | 
			
		||||
            L2 = get_landmarks_nominatim(city_country, l_nature, NATURE)
 | 
			
		||||
            correct_score(L2, preferences.nature)
 | 
			
		||||
            L += L2
 | 
			
		||||
        
 | 
			
		||||
        # List for shopping
 | 
			
		||||
    L3 = get_landmarks(coordinates, l_shop, LandmarkType(landmark_type='shopping'))
 | 
			
		||||
        if preferences.shopping.score != 0 :
 | 
			
		||||
            L3 = get_landmarks_nominatim(city_country, l_shop, SHOPPING)
 | 
			
		||||
            correct_score(L3, preferences.shopping)
 | 
			
		||||
            L += L3
 | 
			
		||||
 | 
			
		||||
    L = L1 + L2 + L3
 | 
			
		||||
    # Use coordinates
 | 
			
		||||
    elif coordinates is not None :
 | 
			
		||||
 | 
			
		||||
    return cleanup_list(L)
 | 
			
		||||
        # List for sightseeing
 | 
			
		||||
        if preferences.sightseeing.score != 0 :
 | 
			
		||||
            L1 = get_landmarks_coords(coordinates, l_sights, SIGHTSEEING)
 | 
			
		||||
            correct_score(L1, preferences.sightseeing)
 | 
			
		||||
            L += L1
 | 
			
		||||
        
 | 
			
		||||
        # List for nature
 | 
			
		||||
        if preferences.nature.score != 0 :
 | 
			
		||||
            L2 = get_landmarks_coords(coordinates, l_nature, NATURE)
 | 
			
		||||
            correct_score(L2, preferences.nature)
 | 
			
		||||
            L += L2
 | 
			
		||||
        
 | 
			
		||||
        # List for shopping
 | 
			
		||||
        if preferences.shopping.score != 0 :
 | 
			
		||||
            L3 = get_landmarks_coords(coordinates, l_shop, SHOPPING)
 | 
			
		||||
            correct_score(L3, preferences.shopping)
 | 
			
		||||
            L += L3
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return L, cleanup_list(L)
 | 
			
		||||
 | 
			
		||||
# Determines if two locations are close to each other
 | 
			
		||||
def is_close_to(loc1: Tuple[float, float], loc2: Tuple[float, float]) :
 | 
			
		||||
def is_close_to(loc1: Tuple[float, float], loc2: Tuple[float, float])->bool :
 | 
			
		||||
 | 
			
		||||
    alpha = (180*RADIUS_CLOSE_TO)/(6371000*m.pi)
 | 
			
		||||
    if abs(loc1[0] - loc2[0]) + abs(loc1[1] - loc2[1]) < alpha*2 :
 | 
			
		||||
@@ -49,7 +83,7 @@ def is_close_to(loc1: Tuple[float, float], loc2: Tuple[float, float]) :
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
# Remove duplicate elements and elements with low score
 | 
			
		||||
def cleanup_list(L: List[Landmark]) :
 | 
			
		||||
def cleanup_list(L: List[Landmark])->List[Landmark] :
 | 
			
		||||
    L_clean = []
 | 
			
		||||
    names = []
 | 
			
		||||
 | 
			
		||||
@@ -70,7 +104,6 @@ def cleanup_list(L: List[Landmark]) :
 | 
			
		||||
    
 | 
			
		||||
    return L_clean
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Correct the score of a list of landmarks by taking into account preferences and the number of tags
 | 
			
		||||
def correct_score(L: List[Landmark], preference: Preference) :
 | 
			
		||||
 | 
			
		||||
@@ -81,8 +114,8 @@ def correct_score(L: List[Landmark], preference: Preference) :
 | 
			
		||||
        raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}")
 | 
			
		||||
 | 
			
		||||
    for elem in L :
 | 
			
		||||
        elem.attractiveness = int(elem.attractiveness/100) + elem.n_tags      # arbitrary correction of the balance score vs number of tags
 | 
			
		||||
        elem.attractiveness = elem.attractiveness*preference.score        # arbitrary computation
 | 
			
		||||
        elem.attractiveness = int(elem.attractiveness) + elem.n_tags*100        # arbitrary correction of the balance score vs number of tags
 | 
			
		||||
        elem.attractiveness = int(elem.attractiveness*preference.score/500)     # arbitrary computation
 | 
			
		||||
 | 
			
		||||
# Correct the score of a list of landmarks by taking into account preferences and the number of tags
 | 
			
		||||
def correct_score_test(L: List[Landmark], preference: Preference) :
 | 
			
		||||
@@ -103,7 +136,9 @@ def count_elements_within_radius(coordinates: Tuple[float, float]) -> int:
 | 
			
		||||
    lat = coordinates[0]
 | 
			
		||||
    lon = coordinates[1]
 | 
			
		||||
 | 
			
		||||
    bbox = {'latLower':lat-RADIUS,'lonLower':lon-RADIUS,'latHigher':lat+RADIUS,'lonHigher': lon+RADIUS}
 | 
			
		||||
    alpha = (180*RADIUS_CLOSE_TO)/(6371000*m.pi)
 | 
			
		||||
 | 
			
		||||
    bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
 | 
			
		||||
    overpass = Overpass()
 | 
			
		||||
    
 | 
			
		||||
    # Build the query to find elements within the radius
 | 
			
		||||
@@ -148,11 +183,11 @@ def create_bbox(coordinates: Tuple[float, float], side_length: int) -> Tuple[flo
 | 
			
		||||
    return min_lat, min_lon, max_lat, max_lon
 | 
			
		||||
 | 
			
		||||
# Generates the list of landmarks for a given Landmarktype. Needs coordinates, a list of amenities and the corresponding LandmarkType
 | 
			
		||||
def get_landmarks(coordinates: Tuple[float, float], l: List[Landmark], landmarktype: LandmarkType):
 | 
			
		||||
def get_landmarks_coords(coordinates: Tuple[float, float], l: List[Landmark], landmarktype: LandmarkType)->List[Landmark]:
 | 
			
		||||
 | 
			
		||||
    overpass = Overpass()
 | 
			
		||||
 | 
			
		||||
    # Generate a bbox around currunt coordinates
 | 
			
		||||
    # Generate a bbox around current coordinates
 | 
			
		||||
    bbox = create_bbox(coordinates, BBOX_SIDE)
 | 
			
		||||
 | 
			
		||||
    # Initialize some variables
 | 
			
		||||
@@ -164,6 +199,46 @@ def get_landmarks(coordinates: Tuple[float, float], l: List[Landmark], landmarkt
 | 
			
		||||
        result = overpass.query(query)
 | 
			
		||||
        N += result.countElements()
 | 
			
		||||
 | 
			
		||||
        for elem in result.elements():
 | 
			
		||||
 | 
			
		||||
            name = elem.tag('name')                             # Add name, decode to ASCII
 | 
			
		||||
            location = (elem.centerLat(), elem.centerLon())     # Add coordinates (lat, lon)
 | 
			
		||||
 | 
			
		||||
            # skip if unprecise location
 | 
			
		||||
            if name is None or location[0] is None:
 | 
			
		||||
                continue
 | 
			
		||||
            else :
 | 
			
		||||
                
 | 
			
		||||
                osm_type = elem.type()              # Add type : 'way' or 'relation'
 | 
			
		||||
                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
 | 
			
		||||
                
 | 
			
		||||
                # Add score of given landmark based on the number of surrounding elements
 | 
			
		||||
                score = count_elements_within_radius(location)
 | 
			
		||||
 | 
			
		||||
                if score is not None :
 | 
			
		||||
                    # 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=n_tags)
 | 
			
		||||
                    L.append(landmark)
 | 
			
		||||
 | 
			
		||||
    return L
 | 
			
		||||
 | 
			
		||||
def get_landmarks_nominatim(city_country: str, l: List[Landmark], landmarktype: LandmarkType)->List[Landmark] :
 | 
			
		||||
    
 | 
			
		||||
    overpass = Overpass()
 | 
			
		||||
    nominatim = Nominatim()
 | 
			
		||||
    areaId = nominatim.query(city_country).areaId()
 | 
			
		||||
    
 | 
			
		||||
    # Initialize some variables
 | 
			
		||||
    N = 0
 | 
			
		||||
    L = []
 | 
			
		||||
 | 
			
		||||
    for amenity in l :
 | 
			
		||||
        query = overpassQueryBuilder(area=areaId, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body')
 | 
			
		||||
        result = overpass.query(query)
 | 
			
		||||
        N += result.countElements()
 | 
			
		||||
 | 
			
		||||
        for elem in result.elements():
 | 
			
		||||
 | 
			
		||||
            name = elem.tag('name')                             # Add name
 | 
			
		||||
@@ -188,4 +263,3 @@ def get_landmarks(coordinates: Tuple[float, float], l: List[Landmark], landmarkt
 | 
			
		||||
                    L.append(landmark)
 | 
			
		||||
 | 
			
		||||
    return L
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,13 +12,13 @@ app = FastAPI()
 | 
			
		||||
 | 
			
		||||
# Assuming frontend is calling like this : 
 | 
			
		||||
#"http://127.0.0.1:8000/process?param1={param1}¶m2={param2}"
 | 
			
		||||
# This should become main at some point
 | 
			
		||||
@app.post("/optimizer/{longitude}/{latitude}")
 | 
			
		||||
def main(longitude: float, latitude: float, preferences: Preferences = Body(...)) -> List[Landmark]:
 | 
			
		||||
@app.post("/optimizer_coords/{longitude}/{latitude}/{city_country}")
 | 
			
		||||
def main1(preferences: Preferences = Body(...), longitude: float = None, latitude: float = None, city_country: str = None) -> List[Landmark]:
 | 
			
		||||
    
 | 
			
		||||
    # From frontend get longitude, latitude and prefence list
 | 
			
		||||
    
 | 
			
		||||
    # Generate the landmark list
 | 
			
		||||
    landmarks = generate_landmarks(tuple((longitude, latitude)), preferences)
 | 
			
		||||
    landmarks = generate_landmarks(preferences=preferences, city_country=city_country, coordinates=tuple((longitude, latitude)))
 | 
			
		||||
 | 
			
		||||
    # Set the max distance
 | 
			
		||||
    max_steps = 90
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,40 @@
 | 
			
		||||
from scipy.optimize import linprog
 | 
			
		||||
import numpy as np
 | 
			
		||||
 | 
			
		||||
from typing import List
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
from scipy.optimize import linprog
 | 
			
		||||
from scipy.linalg import block_diag
 | 
			
		||||
from structs.landmarks import Landmark, LandmarkType
 | 
			
		||||
from structs.preferences import Preference, Preferences
 | 
			
		||||
from structs.landmarks import Landmark
 | 
			
		||||
from math import radians, sin, cos, acos
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DETOUR_FACTOR = 1.3         # detour factor for straightline distance
 | 
			
		||||
AVG_WALKING_SPEED = 4.8     # average walking speed in km/h
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Function that returns the distance in meters from one location to another
 | 
			
		||||
def get_distance(p1: Tuple[float, float], p2: Tuple[float, float]) :
 | 
			
		||||
    
 | 
			
		||||
    # Compute the straight-line distance in km
 | 
			
		||||
    if p1 == p2 :
 | 
			
		||||
        return 0, 0
 | 
			
		||||
    else: 
 | 
			
		||||
        dist = 6371.01 * acos(sin(radians(p1[0]))*sin(radians(p2[0])) + cos(radians(p1[0]))*cos(radians(p2[0]))*cos(radians(p1[1]) - radians(p2[1])))
 | 
			
		||||
 | 
			
		||||
    # Consider the detour factor for average city
 | 
			
		||||
    wdist = dist*DETOUR_FACTOR
 | 
			
		||||
 | 
			
		||||
    # Time to walk this distance (in minutes)
 | 
			
		||||
    wtime = wdist/AVG_WALKING_SPEED*60
 | 
			
		||||
 | 
			
		||||
    if wtime > 15 :
 | 
			
		||||
        wtime = 5*round(wtime/5)
 | 
			
		||||
    else :
 | 
			
		||||
        wtime = round(wtime)
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
    return round(wdist, 1), wtime
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# landmarks = [Landmark_1, Landmark_2, ...]
 | 
			
		||||
@@ -35,10 +67,10 @@ def untangle(resx: list) -> list:
 | 
			
		||||
 | 
			
		||||
    return order
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
# Just to print the result
 | 
			
		||||
def print_res(res, landmarks: list, P) -> list:
 | 
			
		||||
    X = abs(res.x)
 | 
			
		||||
    order = untangle(X)
 | 
			
		||||
def print_res(res, order, landmarks: List[Landmark], P) -> list:
 | 
			
		||||
 | 
			
		||||
    things = []
 | 
			
		||||
 | 
			
		||||
    """N = int(np.sqrt(len(X)))
 | 
			
		||||
@@ -49,7 +81,7 @@ def print_res(res, landmarks: list, P) -> list:
 | 
			
		||||
    for i,x in enumerate(X) : X[i] = round(x,0)
 | 
			
		||||
    print(order)"""
 | 
			
		||||
 | 
			
		||||
    if (X.sum()+1)**2 == len(X) : 
 | 
			
		||||
    if len(order) == len(landmarks): 
 | 
			
		||||
        print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
 | 
			
		||||
    else :
 | 
			
		||||
        print('Could not visit all the landmarks, the following order is suggested : ')
 | 
			
		||||
@@ -59,10 +91,141 @@ def print_res(res, landmarks: list, P) -> list:
 | 
			
		||||
        things.append(landmarks[idx].name)
 | 
			
		||||
 | 
			
		||||
    steps = path_length(P, abs(res.x))
 | 
			
		||||
    print("\nSteps walked : " + str(steps))
 | 
			
		||||
    print("\nMinutes walked : " + str(steps))
 | 
			
		||||
    print(f"\nVisited {len(order)} out of {len(landmarks)} landmarks")
 | 
			
		||||
 | 
			
		||||
    return things
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# prevent the creation of similar circles
 | 
			
		||||
def prevent_circle(resx, landmarks: List[Landmark], A_ub, b_ub) -> bool:
 | 
			
		||||
    for i, elem in enumerate(resx):
 | 
			
		||||
        resx[i] = round(elem)
 | 
			
		||||
    
 | 
			
		||||
    N = len(resx)               # length of res
 | 
			
		||||
    L = int(np.sqrt(N))         # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
 | 
			
		||||
    n_edges = resx.sum()        # number of edges
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
 | 
			
		||||
 | 
			
		||||
    nonzero_tup = np.unravel_index(nonzeroind, (L,L))
 | 
			
		||||
 | 
			
		||||
    ind_a = nonzero_tup[0].tolist()
 | 
			
		||||
    vertices_visited = ind_a
 | 
			
		||||
    vertices_visited.remove(0)
 | 
			
		||||
 | 
			
		||||
    ones = [1]*L
 | 
			
		||||
    h = [0]*N
 | 
			
		||||
    for i in range(L) :
 | 
			
		||||
        if i in vertices_visited :
 | 
			
		||||
            h[i*L:i*L+L] = ones
 | 
			
		||||
 | 
			
		||||
    A_ub = np.vstack((A_ub, h))
 | 
			
		||||
    b_ub.append(len(vertices_visited)-1)
 | 
			
		||||
 | 
			
		||||
    return A_ub, b_ub
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def break_circle2(circle_vertices, landmarks: List[Landmark], A_ub, b_ub) -> bool:
 | 
			
		||||
    
 | 
			
		||||
    L = len(landmarks)         # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if L-1 in circle_vertices :
 | 
			
		||||
        circle_vertices.remove(L-1)
 | 
			
		||||
 | 
			
		||||
    ones = [1]*L
 | 
			
		||||
    h = [0]*L*L
 | 
			
		||||
    for i in range(L) :
 | 
			
		||||
        if i in circle_vertices :
 | 
			
		||||
            h[i*L:i*L+L] = ones
 | 
			
		||||
 | 
			
		||||
    A_ub = np.vstack((A_ub, h))
 | 
			
		||||
    b_ub.append(len(circle_vertices)-1)
 | 
			
		||||
 | 
			
		||||
    return A_ub, b_ub
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Checks if the path is connected
 | 
			
		||||
def is_connected(resx, landmarks: List[Landmark]) -> bool:
 | 
			
		||||
    for i, elem in enumerate(resx):
 | 
			
		||||
        resx[i] = round(elem)
 | 
			
		||||
    
 | 
			
		||||
    N = len(resx)               # length of res
 | 
			
		||||
    L = int(np.sqrt(N))         # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
 | 
			
		||||
    n_edges = resx.sum()        # number of edges
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
 | 
			
		||||
 | 
			
		||||
    nonzero_tup = np.unravel_index(nonzeroind, (L,L))
 | 
			
		||||
 | 
			
		||||
    ind_a = nonzero_tup[0].tolist()
 | 
			
		||||
    ind_b = nonzero_tup[1].tolist()
 | 
			
		||||
 | 
			
		||||
    edges = []
 | 
			
		||||
    edges_visited = []
 | 
			
		||||
    vertices_visited = []
 | 
			
		||||
 | 
			
		||||
    edge1 = (ind_a[0], ind_b[0])
 | 
			
		||||
    edges_visited.append(edge1)
 | 
			
		||||
    vertices_visited.append(edge1[0])
 | 
			
		||||
 | 
			
		||||
    for i, a in enumerate(ind_a) :
 | 
			
		||||
        edges.append((a, ind_b[i]))      # Create the list of edges
 | 
			
		||||
 | 
			
		||||
    flag = False
 | 
			
		||||
 | 
			
		||||
    remaining = edges
 | 
			
		||||
    remaining.remove(edge1)
 | 
			
		||||
    # This can be further optimized
 | 
			
		||||
    #while len(vertices_visited) < n_edges + 1 :
 | 
			
		||||
    break_flag = False
 | 
			
		||||
    while len(remaining) > 0 and not break_flag:
 | 
			
		||||
        for edge2 in remaining :
 | 
			
		||||
            if edge2[0] == edge1[1] :
 | 
			
		||||
                if edge1[1] in vertices_visited :
 | 
			
		||||
                    edges_visited.append(edge2)
 | 
			
		||||
                    break_flag = True
 | 
			
		||||
                    break
 | 
			
		||||
                    #continue # continue vs break vs needed at all ?
 | 
			
		||||
                else :    
 | 
			
		||||
                    vertices_visited.append(edge1[1])
 | 
			
		||||
                    edges_visited.append(edge2)
 | 
			
		||||
                    remaining.remove(edge2)
 | 
			
		||||
                    edge1 = edge2
 | 
			
		||||
 | 
			
		||||
            elif edge1[1] == L-1 or edge1[1] in vertices_visited:
 | 
			
		||||
                        break_flag = True
 | 
			
		||||
                        break
 | 
			
		||||
                #break
 | 
			
		||||
        #if flag is True :
 | 
			
		||||
        #    break
 | 
			
		||||
    vertices_visited.append(edge1[1])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if len(vertices_visited) == n_edges +1 :
 | 
			
		||||
        flag = True
 | 
			
		||||
        circle = []
 | 
			
		||||
    else: 
 | 
			
		||||
        flag = False
 | 
			
		||||
        circle = edges_visited
 | 
			
		||||
 | 
			
		||||
    """j = 0
 | 
			
		||||
    for i in vertices_visited :
 | 
			
		||||
        if landmarks[i].name == 'start' :
 | 
			
		||||
            ordered_visit = vertices_visited[j:] + vertices_visited[:j]
 | 
			
		||||
            break
 | 
			
		||||
        j+=1"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return flag, vertices_visited, circle
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Checks for cases of circular symmetry in the result
 | 
			
		||||
def has_circle(resx: list) :
 | 
			
		||||
    N = len(resx)               # length of res
 | 
			
		||||
@@ -127,13 +290,13 @@ def break_sym(N, A_ub, b_ub):
 | 
			
		||||
    return A_ub, b_ub
 | 
			
		||||
 | 
			
		||||
# Constraint to not have circular paths. Want to go from start -> finish without unconnected loops
 | 
			
		||||
def break_circle(N, A_ub, b_ub, circle) :
 | 
			
		||||
    l = [0]*N*N
 | 
			
		||||
def break_circle(L, A_ub, b_ub, circle) :
 | 
			
		||||
    l = [0]*L*L
 | 
			
		||||
 | 
			
		||||
    for index in circle :
 | 
			
		||||
        x = index[0]
 | 
			
		||||
        y = index[1]
 | 
			
		||||
        l[x*N+y] = 1
 | 
			
		||||
        l[x*L+y] = 1
 | 
			
		||||
 | 
			
		||||
    A_ub = np.vstack((A_ub,l))
 | 
			
		||||
    b_ub.append(len(circle)-1)
 | 
			
		||||
@@ -147,19 +310,29 @@ def break_circle(N, A_ub, b_ub, circle) :
 | 
			
		||||
 | 
			
		||||
# Constraint to respect max number of travels
 | 
			
		||||
def respect_number(N, A_ub, b_ub):
 | 
			
		||||
    h = []
 | 
			
		||||
    """h = []
 | 
			
		||||
    for i in range(N) : h.append([1]*N)
 | 
			
		||||
    T = block_diag(*h)
 | 
			
		||||
    """for l in T :
 | 
			
		||||
    for l in T :
 | 
			
		||||
        for i in range(7):
 | 
			
		||||
            print(l[i*7:i*7+7])
 | 
			
		||||
        print("\n")"""
 | 
			
		||||
    return np.vstack((A_ub, T)), b_ub + [1]*N
 | 
			
		||||
    #return np.vstack((A_ub, T)), b_ub + [1]*N
 | 
			
		||||
    ones = [1]*N
 | 
			
		||||
    zeros = [0]*N
 | 
			
		||||
    for i in range(N) :
 | 
			
		||||
        h = zeros*i + ones + zeros*(N-1-i)
 | 
			
		||||
 | 
			
		||||
        A_ub = np.vstack((A_ub, h))
 | 
			
		||||
        b_ub.append(1)
 | 
			
		||||
 | 
			
		||||
    return A_ub, b_ub
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
 | 
			
		||||
def respect_order(N: int, A_eq, b_eq): 
 | 
			
		||||
    for i in range(N-1) :     # Prevent stacked ones
 | 
			
		||||
        if i == 0 :
 | 
			
		||||
        if i == 0 or i == N-1:  # Don't touch start or finish
 | 
			
		||||
            continue
 | 
			
		||||
        else : 
 | 
			
		||||
            l = [0]*N
 | 
			
		||||
@@ -171,10 +344,6 @@ def respect_order(N: int, A_eq, b_eq):
 | 
			
		||||
            A_eq = np.vstack((A_eq,l))
 | 
			
		||||
            b_eq.append(0)
 | 
			
		||||
 | 
			
		||||
            """for i in range(7):
 | 
			
		||||
                print(l[i*7:i*7+7])
 | 
			
		||||
            print("\n")"""
 | 
			
		||||
 | 
			
		||||
    return A_eq, b_eq
 | 
			
		||||
 | 
			
		||||
# Compute manhattan distance between 2 locations
 | 
			
		||||
@@ -183,27 +352,41 @@ def manhattan_distance(loc1: tuple, loc2: tuple):
 | 
			
		||||
    x2, y2 = loc2
 | 
			
		||||
    return abs(x1 - x2) + abs(y1 - y2)
 | 
			
		||||
 | 
			
		||||
# Constraint to not stay in position
 | 
			
		||||
# Constraint to not stay in position. Removes d11, d22, d33, etc.
 | 
			
		||||
def init_eq_not_stay(N: int): 
 | 
			
		||||
    l = [0]*N*N
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    for i in range(N) :
 | 
			
		||||
        for j in range(N) :
 | 
			
		||||
            if j == i :
 | 
			
		||||
                l[j + i*N] = 1
 | 
			
		||||
    l[N-1] = 1      # cannot skip from start to finish
 | 
			
		||||
    #A_eq = np.array([np.array(xi) for xi in A_eq])                  # Must convert A_eq into an np array
 | 
			
		||||
    l = np.array(np.array(l))
 | 
			
		||||
    
 | 
			
		||||
    """for i in range(7):
 | 
			
		||||
        print(l[i*7:i*7+7])"""
 | 
			
		||||
    l = np.array(np.array(l))
 | 
			
		||||
 | 
			
		||||
    return [l], [0]
 | 
			
		||||
 | 
			
		||||
# Constraint to ensure start at start and finish at goal
 | 
			
		||||
def respect_start_finish(N, A_eq: list, b_eq: list):
 | 
			
		||||
    ls = [1]*N + [0]*N*(N-1)    # sets only horizontal ones for start (go from)
 | 
			
		||||
    ljump = [0]*N*N
 | 
			
		||||
    ljump[N-1] = 1              # Prevent start finish jump
 | 
			
		||||
    lg = [0]*N*N
 | 
			
		||||
    for k in range(N-1) :       # sets only vertical ones for goal (go to)
 | 
			
		||||
        if k != 0 :             # Prevent the shortcut start -> finish
 | 
			
		||||
            lg[k*N+N-1] = 1 
 | 
			
		||||
 | 
			
		||||
    A_eq = np.vstack((A_eq,ls))
 | 
			
		||||
    A_eq = np.vstack((A_eq,ljump))
 | 
			
		||||
    A_eq = np.vstack((A_eq,lg))
 | 
			
		||||
    b_eq.append(1)
 | 
			
		||||
    b_eq.append(0)
 | 
			
		||||
    b_eq.append(1)
 | 
			
		||||
 | 
			
		||||
    return A_eq, b_eq
 | 
			
		||||
 | 
			
		||||
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
 | 
			
		||||
# We want to maximize the sightseeing :  max(c) st. A*x < b   and   A_eq*x = b_eq
 | 
			
		||||
def init_ub_dist(landmarks: list, max_steps: int):
 | 
			
		||||
def init_ub_dist(landmarks: List[Landmark], max_steps: int):
 | 
			
		||||
    # Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
 | 
			
		||||
    c = []
 | 
			
		||||
    # Coefficients of inequality constraints (left-hand side)
 | 
			
		||||
@@ -212,7 +395,8 @@ def init_ub_dist(landmarks: list, max_steps: int):
 | 
			
		||||
        dist_table = [0]*len(landmarks)
 | 
			
		||||
        c.append(-spot1.attractiveness)
 | 
			
		||||
        for j, spot2 in enumerate(landmarks) :
 | 
			
		||||
            dist_table[j] = manhattan_distance(spot1.location, spot2.location)
 | 
			
		||||
            d, t = get_distance(spot1.location, spot2.location)
 | 
			
		||||
            dist_table[j] = t
 | 
			
		||||
        A.append(dist_table)
 | 
			
		||||
    c = c*len(landmarks)
 | 
			
		||||
    A_ub = []
 | 
			
		||||
@@ -222,31 +406,31 @@ def init_ub_dist(landmarks: list, max_steps: int):
 | 
			
		||||
    return c, A_ub, [max_steps]
 | 
			
		||||
 | 
			
		||||
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
 | 
			
		||||
def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
 | 
			
		||||
def respect_user_mustsee(landmarks: List[Landmark], A_eq: list, b_eq: list) :
 | 
			
		||||
    L = len(landmarks)
 | 
			
		||||
    H = 0       # sort of heuristic to get an idea of the number of steps needed
 | 
			
		||||
    for i in landmarks : 
 | 
			
		||||
        if i.name == "départ" : elem_prev = i              # list of all matches
 | 
			
		||||
    for i, elem in enumerate(landmarks) :
 | 
			
		||||
        if elem.attractiveness == -1 :
 | 
			
		||||
            l = [0]*L*L
 | 
			
		||||
            if elem.name != "arrivée" :
 | 
			
		||||
                for j in range(L) :
 | 
			
		||||
                    l[j +i*L] = 1
 | 
			
		||||
    
 | 
			
		||||
            else :                          # This ensures we go to goal
 | 
			
		||||
    elem_prev = landmarks[0]
 | 
			
		||||
    
 | 
			
		||||
    for i, elem in enumerate(landmarks) :
 | 
			
		||||
        if elem.must_do is True and elem.name not in ['finish', 'start']:
 | 
			
		||||
            l = [0]*L*L
 | 
			
		||||
            for j in range(L) :     # sets the horizontal ones (go from)
 | 
			
		||||
                l[j +i*L] = 1       # sets the vertical ones (go to)        double check if good
 | 
			
		||||
                
 | 
			
		||||
            for k in range(L-1) :
 | 
			
		||||
                l[k*L+L-1] = 1  
 | 
			
		||||
 | 
			
		||||
            H += manhattan_distance(elem.location, elem_prev.location)
 | 
			
		||||
            elem_prev = elem
 | 
			
		||||
 | 
			
		||||
            """for i in range(7):
 | 
			
		||||
                print(l[i*7:i*7+7])
 | 
			
		||||
            print("\n")"""
 | 
			
		||||
 | 
			
		||||
            A_eq = np.vstack((A_eq,l))
 | 
			
		||||
            b_eq.append(1)
 | 
			
		||||
            b_eq.append(2)
 | 
			
		||||
 | 
			
		||||
        d, t = get_distance(elem.location, elem_prev.location)
 | 
			
		||||
        H += t
 | 
			
		||||
        elem_prev = elem
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
    return A_eq, b_eq, H
 | 
			
		||||
 | 
			
		||||
# Computes the path length given path matrix (dist_table) and a result
 | 
			
		||||
@@ -261,7 +445,7 @@ def solve_optimization (landmarks, max_steps, printing_details) :
 | 
			
		||||
    # SET CONSTRAINTS FOR INEQUALITY
 | 
			
		||||
    c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)      # Add the distances from each landmark to the other
 | 
			
		||||
    P = A_ub                                                # store the paths for later. Needed to compute path length
 | 
			
		||||
    A_ub, b_ub = respect_number(N, A_ub, b_ub)              # Respect max number of visits. 
 | 
			
		||||
    A_ub, b_ub = respect_number(N, A_ub, b_ub)              # Respect max number of visits (no more possible stops than landmarks). 
 | 
			
		||||
 | 
			
		||||
    # TODO : Problems with circular symmetry
 | 
			
		||||
    A_ub, b_ub = break_sym(N, A_ub, b_ub)                  # break the symmetry. Only use the upper diagonal values
 | 
			
		||||
@@ -269,7 +453,7 @@ def solve_optimization (landmarks, max_steps, printing_details) :
 | 
			
		||||
    # SET CONSTRAINTS FOR EQUALITY
 | 
			
		||||
    A_eq, b_eq = init_eq_not_stay(N)                                # Force solution not to stay in same place
 | 
			
		||||
    A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq)     # Check if there are user_defined must_see. Also takes care of start/goal
 | 
			
		||||
 | 
			
		||||
    A_eq, b_eq = respect_start_finish(N, A_eq, b_eq)                # Force start and finish positions
 | 
			
		||||
    A_eq, b_eq = respect_order(N, A_eq, b_eq)                       # Respect order of visit (only works when max_steps is limiting factor)
 | 
			
		||||
 | 
			
		||||
    # Bounds for variables (x can only be 0 or 1)
 | 
			
		||||
@@ -298,20 +482,27 @@ def solve_optimization (landmarks, max_steps, printing_details) :
 | 
			
		||||
    
 | 
			
		||||
    # If there is a solution, we're good to go, just check for
 | 
			
		||||
    else :
 | 
			
		||||
        circle = has_circle(res.x)
 | 
			
		||||
        t, order, circle = is_connected(res.x, landmarks)
 | 
			
		||||
        i = 0
 | 
			
		||||
 | 
			
		||||
        # Break the circular symmetry if needed
 | 
			
		||||
        while len(circle) != 0 :
 | 
			
		||||
            A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle)
 | 
			
		||||
            A_ub, b_ub = prevent_circle(res.x, landmarks, A_ub, b_ub)
 | 
			
		||||
            A_ub, b_ub = break_circle(len(landmarks), A_ub, b_ub, circle)
 | 
			
		||||
            A_ub, b_ub = break_circle2(order, landmarks, A_ub, b_ub)
 | 
			
		||||
            res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
 | 
			
		||||
            circle = has_circle(res.x)
 | 
			
		||||
            t, order, circle = is_connected(res.x, landmarks)
 | 
			
		||||
            if t :
 | 
			
		||||
                break
 | 
			
		||||
            #circle = has_circle(res.x)
 | 
			
		||||
            print(i)
 | 
			
		||||
            i += 1
 | 
			
		||||
 | 
			
		||||
        if printing_details is True :
 | 
			
		||||
            if i != 0 :
 | 
			
		||||
                print(f"Neded to recompute paths {i} times because of unconnected loops...")
 | 
			
		||||
            X = print_res(res, landmarks, P)
 | 
			
		||||
            t, order, [] = is_connected(res.x, landmarks)
 | 
			
		||||
            X = print_res(res, order, landmarks, P)
 | 
			
		||||
            return X
 | 
			
		||||
        else :
 | 
			
		||||
            return untangle(res.x)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,25 @@
 | 
			
		||||
import pandas as pd
 | 
			
		||||
from optimizer import solve_optimization
 | 
			
		||||
from landmarks_manager import generate_landmarks
 | 
			
		||||
from structs.landmarks import LandmarkTest
 | 
			
		||||
from structs.landmarks import Landmark
 | 
			
		||||
from structs.landmarktype import LandmarkType
 | 
			
		||||
from structs.preferences import Preferences, Preference
 | 
			
		||||
from fastapi import FastAPI, Query, Body
 | 
			
		||||
from fastapi.encoders import jsonable_encoder
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Helper function to create a .txt file with results
 | 
			
		||||
def write_data(L: List[Landmark]): 
 | 
			
		||||
 | 
			
		||||
    data = pd.DataFrame()
 | 
			
		||||
    i = 0
 | 
			
		||||
 | 
			
		||||
    for landmark in L :
 | 
			
		||||
        data[i] = jsonable_encoder(landmark)
 | 
			
		||||
        i += 1
 | 
			
		||||
 | 
			
		||||
    data.to_json('landmarks.txt', indent = 2, force_ascii=False)
 | 
			
		||||
 | 
			
		||||
def test3(city_country: str) -> List[Landmark]:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +37,9 @@ def test3(city_country: str) -> List[Landmark]:
 | 
			
		||||
                                  type=LandmarkType(landmark_type='shopping'),
 | 
			
		||||
                                  score = 5))
 | 
			
		||||
 | 
			
		||||
    landmarks = generate_landmarks(city_country, preferences)
 | 
			
		||||
    coords = None
 | 
			
		||||
 | 
			
		||||
    landmarks = generate_landmarks(preferences=preferences, city_country=city_country, coordinates=coords)
 | 
			
		||||
 | 
			
		||||
    max_steps = 9
 | 
			
		||||
 | 
			
		||||
@@ -47,21 +61,33 @@ def test4(coordinates: tuple[float, float]) -> List[Landmark]:
 | 
			
		||||
                    nature=Preference(
 | 
			
		||||
                                  name='nature', 
 | 
			
		||||
                                  type=LandmarkType(landmark_type='nature'),
 | 
			
		||||
                                  score = 0),
 | 
			
		||||
                                  score = 5),
 | 
			
		||||
                    shopping=Preference(
 | 
			
		||||
                                  name='shopping', 
 | 
			
		||||
                                  type=LandmarkType(landmark_type='shopping'),
 | 
			
		||||
                                  score = 5))
 | 
			
		||||
 | 
			
		||||
    landmarks = generate_landmarks(coordinates, preferences)
 | 
			
		||||
    city_country = None
 | 
			
		||||
 | 
			
		||||
    max_steps = 90
 | 
			
		||||
    landmarks, landmarks_short = generate_landmarks(preferences=preferences, city_country=city_country, coordinates=coordinates)
 | 
			
		||||
 | 
			
		||||
    visiting_order = solve_optimization(landmarks, max_steps, True)
 | 
			
		||||
    #write_data(landmarks)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.8375946, 2.2949904), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
 | 
			
		||||
    finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.8375946, 2.2949904), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    test = landmarks_short
 | 
			
		||||
    test.append(finish)
 | 
			
		||||
    test.insert(0, start)
 | 
			
		||||
 | 
			
		||||
    max_walking_time = 4 # hours
 | 
			
		||||
 | 
			
		||||
    visiting_order = solve_optimization(test, max_walking_time*60, True)
 | 
			
		||||
 | 
			
		||||
    print(len(visiting_order))
 | 
			
		||||
 | 
			
		||||
    return len(visiting_order)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
test3(tuple((48.834378, 2.322113)))
 | 
			
		||||
test4(tuple((48.834378, 2.322113)))
 | 
			
		||||
							
								
								
									
										20297
									
								
								landmarks.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20297
									
								
								landmarks.txt
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user