420 lines
13 KiB
Python
420 lines
13 KiB
Python
import numpy as np
|
|
import yaml
|
|
|
|
from typing import List, Tuple
|
|
from scipy.optimize import linprog
|
|
from math import radians, sin, cos, acos
|
|
|
|
from structs.landmarks import Landmark
|
|
import constants
|
|
|
|
# Function to print the result
|
|
def print_res(L: List[Landmark], L_tot):
|
|
|
|
if len(L) == L_tot:
|
|
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 : ')
|
|
|
|
dist = 0
|
|
for elem in L :
|
|
if elem.time_to_reach_next is not None :
|
|
print('- ' + elem.name + ', time to reach next = ' + str(elem.time_to_reach_next))
|
|
dist += elem.time_to_reach_next
|
|
else :
|
|
print('- ' + elem.name)
|
|
|
|
print("\nMinutes walked : " + str(dist))
|
|
print(f"Visited {len(L)-2} out of {L_tot-2} landmarks")
|
|
|
|
|
|
# Prevent the use of a particular solution
|
|
def prevent_config(resx, A_ub, b_ub) -> bool:
|
|
|
|
for i, elem in enumerate(resx):
|
|
resx[i] = round(elem)
|
|
|
|
N = len(resx) # Number of edges
|
|
L = int(np.sqrt(N)) # Number of landmarks
|
|
|
|
nonzeroind = np.nonzero(resx)[0] # the return is a little funky 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
|
|
|
|
|
|
# Prevent the possibility of a given solution bit
|
|
def break_cricle(circle_vertices: list, L: int, A_ub: list, b_ub: list) -> bool:
|
|
|
|
if L-1 in circle_vertices :
|
|
circle_vertices.remove(L-1)
|
|
|
|
h = [0]*L*L
|
|
for i in range(L) :
|
|
if i in circle_vertices :
|
|
h[i*L:i*L+L] = [1]*L
|
|
|
|
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, returns a circle if it finds one and the RESULT
|
|
def is_connected(resx) -> bool:
|
|
|
|
# first round the results to have only 0-1 values
|
|
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
|
|
|
|
remaining = edges
|
|
remaining.remove(edge1)
|
|
|
|
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
|
|
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
|
|
|
|
vertices_visited.append(edge1[1])
|
|
|
|
|
|
if len(vertices_visited) == n_edges +1 :
|
|
return vertices_visited, []
|
|
else:
|
|
return vertices_visited, edges_visited
|
|
|
|
|
|
# Function that returns the distance in meters from one location to another
|
|
def get_distance(p1: Tuple[float, float], p2: Tuple[float, float], detour: float, speed: 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 cityto deterline walking distance (in km)
|
|
walk_dist = dist*detour
|
|
|
|
# Time to walk this distance (in minutes)
|
|
walk_time = walk_dist/speed*60
|
|
|
|
if walk_time > 15 :
|
|
walk_time = 5*round(walk_time/5)
|
|
else :
|
|
walk_time = round(walk_time)
|
|
|
|
|
|
return round(walk_dist, 1), walk_time
|
|
|
|
|
|
# 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[Landmark], max_steps: int):
|
|
|
|
# Read the parameters from the file
|
|
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
|
parameters = yaml.safe_load(f)
|
|
detour = parameters['detour_factor']
|
|
speed = parameters['average_walking_speed']
|
|
|
|
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
|
|
c = []
|
|
# Coefficients of inequality constraints (left-hand side)
|
|
A_ub = []
|
|
|
|
for spot1 in landmarks :
|
|
dist_table = [0]*len(landmarks)
|
|
c.append(-spot1.attractiveness)
|
|
for j, spot2 in enumerate(landmarks) :
|
|
t = get_distance(spot1.location, spot2.location, detour, speed)[1]
|
|
dist_table[j] = t
|
|
A_ub += dist_table
|
|
c = c*len(landmarks)
|
|
|
|
return c, A_ub, [max_steps]
|
|
|
|
|
|
# Constraint to respect only one travel per landmark. Also caps the total number of visited landmarks
|
|
def respect_number(L:int, A_ub, b_ub):
|
|
|
|
ones = [1]*L
|
|
zeros = [0]*L
|
|
for i in range(L) :
|
|
h = zeros*i + ones + zeros*(L-1-i)
|
|
A_ub = np.vstack((A_ub, h))
|
|
b_ub.append(1)
|
|
|
|
# Read the parameters from the file
|
|
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
|
parameters = yaml.safe_load(f)
|
|
max_landmarks = parameters['max_landmarks']
|
|
|
|
A_ub = np.vstack((A_ub, ones*L))
|
|
b_ub.append(max_landmarks+1)
|
|
|
|
return A_ub, b_ub
|
|
|
|
|
|
# Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements
|
|
def break_sym(L, A_ub, b_ub):
|
|
upper_ind = np.triu_indices(L,0,L)
|
|
|
|
up_ind_x = upper_ind[0]
|
|
up_ind_y = upper_ind[1]
|
|
|
|
for i, _ in enumerate(up_ind_x) :
|
|
l = [0]*L*L
|
|
if up_ind_x[i] != up_ind_y[i] :
|
|
l[up_ind_x[i]*L + up_ind_y[i]] = 1
|
|
l[up_ind_y[i]*L + up_ind_x[i]] = 1
|
|
|
|
A_ub = np.vstack((A_ub,l))
|
|
b_ub.append(1)
|
|
|
|
return A_ub, b_ub
|
|
|
|
|
|
# Constraint to not stay in position. Removes d11, d22, d33, etc.
|
|
def init_eq_not_stay(L: int):
|
|
l = [0]*L*L
|
|
|
|
for i in range(L) :
|
|
for j in range(L) :
|
|
if j == i :
|
|
l[j + i*L] = 1
|
|
|
|
l = np.array(np.array(l))
|
|
|
|
return [l], [0]
|
|
|
|
|
|
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
|
|
def respect_user_mustsee(landmarks: List[Landmark], A_eq: list, b_eq: list) :
|
|
L = len(landmarks)
|
|
|
|
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
|
|
|
|
A_eq = np.vstack((A_eq,l))
|
|
b_eq.append(2)
|
|
|
|
return A_eq, b_eq
|
|
|
|
|
|
# Constraint to ensure start at start and finish at goal
|
|
def respect_start_finish(L: int, A_eq: list, b_eq: list):
|
|
ls = [1]*L + [0]*L*(L-1) # sets only horizontal ones for start (go from)
|
|
ljump = [0]*L*L
|
|
ljump[L-1] = 1 # Prevent start finish jump
|
|
lg = [0]*L*L
|
|
ll = [0]*L*(L-1) + [1]*L
|
|
for k in range(L-1) : # sets only vertical ones for goal (go to)
|
|
ll[k*L] = 1
|
|
if k != 0 : # Prevent the shortcut start -> finish
|
|
lg[k*L+L-1] = 1
|
|
|
|
|
|
A_eq = np.vstack((A_eq,ls))
|
|
A_eq = np.vstack((A_eq,ljump))
|
|
A_eq = np.vstack((A_eq,lg))
|
|
A_eq = np.vstack((A_eq,ll))
|
|
b_eq.append(1)
|
|
b_eq.append(0)
|
|
b_eq.append(1)
|
|
b_eq.append(0)
|
|
|
|
return A_eq, b_eq
|
|
|
|
|
|
# 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 or i == N-1: # Don't touch start or finish
|
|
continue
|
|
else :
|
|
l = [0]*N
|
|
l[i] = -1
|
|
l = l*N
|
|
for j in range(N) :
|
|
l[i*N + j] = 1
|
|
|
|
A_eq = np.vstack((A_eq,l))
|
|
b_eq.append(0)
|
|
|
|
return A_eq, b_eq
|
|
|
|
|
|
# Computes the time to reach from each landmark to the next
|
|
def link_list(order: List[int], landmarks: List[Landmark]) -> List[Landmark]:
|
|
|
|
# Read the parameters from the file
|
|
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
|
parameters = yaml.safe_load(f)
|
|
|
|
detour_factor = parameters['detour_factor']
|
|
speed = parameters['average_walking_speed']
|
|
|
|
L = []
|
|
j = 0
|
|
total_dist = 0
|
|
while j < len(order)-1 :
|
|
elem = landmarks[order[j]]
|
|
next = landmarks[order[j+1]]
|
|
|
|
d = get_distance(elem.location, next.location, detour_factor, speed)[1]
|
|
elem.time_to_reach_next = d
|
|
L.append(elem)
|
|
j += 1
|
|
total_dist += d
|
|
|
|
L.append(next)
|
|
|
|
return L, total_dist
|
|
|
|
|
|
def link_list_simple(ordered_visit: List[Landmark])-> List[Landmark] :
|
|
|
|
# Read the parameters from the file
|
|
with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
|
parameters = yaml.safe_load(f)
|
|
|
|
detour_factor = parameters['detour_factor']
|
|
speed = parameters['average_walking_speed']
|
|
|
|
L = []
|
|
j = 0
|
|
total_dist = 0
|
|
while j < len(ordered_visit)-1 :
|
|
elem = ordered_visit[j]
|
|
next = ordered_visit[j+1]
|
|
|
|
elem.next_uuid = next.uuid
|
|
d = get_distance(elem.location, next.location, detour_factor, speed)[1]
|
|
elem.time_to_reach_next = d
|
|
if elem.name not in ['start', 'finish'] :
|
|
elem.must_do = True
|
|
L.append(elem)
|
|
j += 1
|
|
total_dist += d
|
|
|
|
L.append(next)
|
|
|
|
return L, total_dist
|
|
|
|
|
|
# Main optimization pipeline
|
|
def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_details: bool) :
|
|
|
|
L = len(landmarks)
|
|
|
|
# SET CONSTRAINTS FOR INEQUALITY
|
|
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
|
|
A_ub, b_ub = respect_number(L, A_ub, b_ub) # Respect max number of visits (no more possible stops than landmarks).
|
|
A_ub, b_ub = break_sym(L, A_ub, b_ub) # break the 'zig-zag' symmetry
|
|
|
|
# SET CONSTRAINTS FOR EQUALITY
|
|
A_eq, b_eq = init_eq_not_stay(L) # Force solution not to stay in same place
|
|
A_eq, b_eq = 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(L, A_eq, b_eq) # Force start and finish positions
|
|
A_eq, b_eq = respect_order(L, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
|
|
|
|
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
|
|
x_bounds = [(0, 1)]*L*L
|
|
|
|
# Solve linear programming problem
|
|
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)
|
|
|
|
# Raise error if no solution is found
|
|
if not res.success :
|
|
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
|
|
|
|
# If there is a solution, we're good to go, just check for connectiveness
|
|
else :
|
|
order, circle = is_connected(res.x)
|
|
i = 0
|
|
timeout = 80
|
|
while len(circle) != 0 and i < timeout:
|
|
A_ub, b_ub = prevent_config(res.x, A_ub, b_ub)
|
|
A_ub, b_ub = break_cricle(order, len(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)
|
|
order, circle = is_connected(res.x)
|
|
if len(circle) == 0 :
|
|
break
|
|
print(i)
|
|
i += 1
|
|
|
|
if i == timeout :
|
|
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
|
|
|
# Add the times to reach and stop optimizing
|
|
L, total_dist = link_list(order, landmarks)
|
|
|
|
if printing_details is True :
|
|
if i != 0 :
|
|
print(f"Neded to recompute paths {i} times because of unconnected loops...")
|
|
print_res(L, len(landmarks))
|
|
print("\nTotal score : " + str(int(-res.fun)))
|
|
|
|
return L
|
|
|
|
|
|
|
|
|
|
|
|
|