massive numpy optimization and more tests
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Failing after 2m17s
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been skipped
Run linting on the backend code / Build (pull_request) Successful in 25s
Run testing on the backend code / Build (pull_request) Failing after 6m58s
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Failing after 2m17s
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been skipped
Run linting on the backend code / Build (pull_request) Successful in 25s
Run testing on the backend code / Build (pull_request) Failing after 6m58s
This commit is contained in:
parent
4fae658dbb
commit
ecd505a9ce
1094
backend/report.html
Normal file
1094
backend/report.html
Normal file
File diff suppressed because one or more lines are too long
@ -109,9 +109,12 @@ def new_trip(preferences: Preferences,
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# Second stage optimization
|
# Second stage optimization
|
||||||
|
try :
|
||||||
refined_tour = refiner.refine_optimization(landmarks, base_tour,
|
refined_tour = refiner.refine_optimization(landmarks, base_tour,
|
||||||
preferences.max_time_minute,
|
preferences.max_time_minute,
|
||||||
preferences.detour_tolerance_minute)
|
preferences.detour_tolerance_minute)
|
||||||
|
except Exception as exc :
|
||||||
|
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(exc)}") from exc
|
||||||
|
|
||||||
t_second_stage = time.time() - start_time
|
t_second_stage = time.time() - start_time
|
||||||
logger.debug(f'Generating landmarks : {round(t_generate_landmarks,3)} seconds')
|
logger.debug(f'Generating landmarks : {round(t_generate_landmarks,3)} seconds')
|
||||||
|
@ -35,7 +35,6 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
result = response.json()
|
result = response.json()
|
||||||
print(result)
|
|
||||||
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
|
||||||
|
|
||||||
|
|
||||||
@ -45,13 +44,16 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
|||||||
# Add details to report
|
# Add details to report
|
||||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||||
|
|
||||||
|
# for elem in landmarks :
|
||||||
|
# print(elem)
|
||||||
|
|
||||||
# checks :
|
# checks :
|
||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
assert isinstance(landmarks, list) # check that the return type is a list
|
assert isinstance(landmarks, list) # check that the return type is a list
|
||||||
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||||
assert len(landmarks) > 2 # check that there is something to visit
|
assert len(landmarks) > 2 # check that there is something to visit
|
||||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||||
assert 2==3
|
# assert 2==3
|
||||||
|
|
||||||
def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
||||||
"""
|
"""
|
||||||
@ -84,16 +86,15 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
|||||||
# Add details to report
|
# Add details to report
|
||||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||||
|
|
||||||
for elem in landmarks :
|
# for elem in landmarks :
|
||||||
print(elem)
|
# print(elem)
|
||||||
print(elem.osm_id)
|
|
||||||
|
|
||||||
# checks :
|
# checks :
|
||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
|
||||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||||
|
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||||
|
# assert 2 == 3
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
def test_Paris(client, request) : # pylint: disable=redefined-outer-name
|
def test_Paris(client, request) : # pylint: disable=redefined-outer-name
|
||||||
"""
|
"""
|
||||||
@ -126,14 +127,13 @@ def test_Paris(client, request) : # pylint: disable=redefined-outer-name
|
|||||||
# Add details to report
|
# Add details to report
|
||||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||||
|
|
||||||
for elem in landmarks :
|
# for elem in landmarks :
|
||||||
print(elem)
|
# print(elem)
|
||||||
print(elem.osm_id)
|
|
||||||
|
|
||||||
# checks :
|
# checks :
|
||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
|
||||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||||
|
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||||
|
|
||||||
|
|
||||||
def test_New_York(client, request) : # pylint: disable=redefined-outer-name
|
def test_New_York(client, request) : # pylint: disable=redefined-outer-name
|
||||||
@ -167,14 +167,14 @@ def test_New_York(client, request) : # pylint: disable=redefined-outer-name
|
|||||||
# Add details to report
|
# Add details to report
|
||||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||||
|
|
||||||
for elem in landmarks :
|
# for elem in landmarks :
|
||||||
print(elem)
|
# print(elem)
|
||||||
print(elem.osm_id)
|
|
||||||
|
|
||||||
# checks :
|
# checks :
|
||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
|
||||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||||
|
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||||
|
|
||||||
|
|
||||||
def test_shopping(client, request) : # pylint: disable=redefined-outer-name
|
def test_shopping(client, request) : # pylint: disable=redefined-outer-name
|
||||||
"""
|
"""
|
||||||
@ -207,11 +207,15 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name
|
|||||||
# Add details to report
|
# Add details to report
|
||||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||||
|
|
||||||
|
# for elem in landmarks :
|
||||||
|
# print(elem)
|
||||||
|
|
||||||
# checks :
|
# checks :
|
||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
|
||||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||||
'''
|
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||||
|
|
||||||
|
|
||||||
# def test_new_trip_single_prefs(client):
|
# def test_new_trip_single_prefs(client):
|
||||||
# response = client.post(
|
# response = client.post(
|
||||||
# "/trip/new",
|
# "/trip/new",
|
||||||
|
@ -42,7 +42,7 @@ def fetch_landmark(client, landmark_uuid: str):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
json_data = response.json()
|
json_data = response.json()
|
||||||
logger.info(f'API Response: {json_data}')
|
# logger.info(f'API Response: {json_data}')
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f'Failed to parse response as JSON: {response.text}')
|
logger.error(f'Failed to parse response as JSON: {response.text}')
|
||||||
raise HTTPException(status_code=500, detail="Invalid response format from API") from e
|
raise HTTPException(status_code=500, detail="Invalid response format from API") from e
|
||||||
|
@ -12,6 +12,10 @@ from ..utils.get_time_separation import get_distance
|
|||||||
from ..constants import OSM_CACHE_DIR
|
from ..constants import OSM_CACHE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
# silence the overpass logger
|
||||||
|
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
class Cluster(BaseModel):
|
class Cluster(BaseModel):
|
||||||
""""
|
""""
|
||||||
A class representing an interesting area for shopping or sightseeing.
|
A class representing an interesting area for shopping or sightseeing.
|
||||||
@ -102,7 +106,6 @@ class ClusterManager:
|
|||||||
points.append(coords)
|
points.append(coords)
|
||||||
|
|
||||||
self.all_points = np.array(points)
|
self.all_points = np.array(points)
|
||||||
self.valid = True
|
|
||||||
|
|
||||||
# Apply DBSCAN to find clusters. Choose different settings for different cities.
|
# Apply DBSCAN to find clusters. Choose different settings for different cities.
|
||||||
if self.cluster_type == 'shopping' and len(self.all_points) > 200 :
|
if self.cluster_type == 'shopping' and len(self.all_points) > 200 :
|
||||||
@ -114,12 +117,17 @@ class ClusterManager:
|
|||||||
|
|
||||||
labels = dbscan.fit_predict(self.all_points)
|
labels = dbscan.fit_predict(self.all_points)
|
||||||
|
|
||||||
|
# Check that there are at least 2 different clusters
|
||||||
|
if len(set(labels)) > 2 :
|
||||||
|
self.logger.debug(f"Found {len(set(labels))} different clusters.")
|
||||||
# Separate clustered points and noise points
|
# Separate clustered points and noise points
|
||||||
self.cluster_points = self.all_points[labels != -1]
|
self.cluster_points = self.all_points[labels != -1]
|
||||||
self.cluster_labels = labels[labels != -1]
|
self.cluster_labels = labels[labels != -1]
|
||||||
|
self.filter_clusters() # ValueError here sometimes. I dont know why. # Filter the clusters to keep only the largest ones.
|
||||||
|
self.valid = True
|
||||||
|
|
||||||
# filter the clusters to keep only the largest ones
|
else :
|
||||||
self.filter_clusters()
|
self.valid = False
|
||||||
|
|
||||||
|
|
||||||
def generate_clusters(self) -> list[Landmark]:
|
def generate_clusters(self) -> list[Landmark]:
|
||||||
|
@ -23,9 +23,9 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
if p1 == p2:
|
# if p1 == p2:
|
||||||
return 0
|
# return 0
|
||||||
else:
|
# else:
|
||||||
# Compute the distance in km along the surface of the Earth
|
# Compute the distance in km along the surface of the Earth
|
||||||
# (assume spherical Earth)
|
# (assume spherical Earth)
|
||||||
# this is the haversine formula, stolen from stackoverflow
|
# this is the haversine formula, stolen from stackoverflow
|
||||||
@ -47,7 +47,7 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
|||||||
# Time to walk this distance (in minutes)
|
# Time to walk this distance (in minutes)
|
||||||
walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
|
walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60
|
||||||
|
|
||||||
return round(walk_time)
|
return min(round(walk_time), 32765)
|
||||||
|
|
||||||
|
|
||||||
def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||||
|
@ -53,6 +53,8 @@ class LandmarkManager:
|
|||||||
self.overpass = Overpass()
|
self.overpass = Overpass()
|
||||||
CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR)
|
CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR)
|
||||||
|
|
||||||
|
self.logger.info('LandmakManager successfully initialized.')
|
||||||
|
|
||||||
|
|
||||||
def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]:
|
def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]:
|
||||||
"""
|
"""
|
||||||
@ -71,7 +73,7 @@ class LandmarkManager:
|
|||||||
- A list of all existing landmarks.
|
- A list of all existing landmarks.
|
||||||
- A list of the most important landmarks based on the user's preferences.
|
- A list of the most important landmarks based on the user's preferences.
|
||||||
"""
|
"""
|
||||||
|
self.logger.debug('Starting to fetch landmarks...')
|
||||||
max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor
|
max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor
|
||||||
reachable_bbox_side = min(max_walk_dist, self.max_bbox_side)
|
reachable_bbox_side = min(max_walk_dist, self.max_bbox_side)
|
||||||
|
|
||||||
@ -83,25 +85,32 @@ class LandmarkManager:
|
|||||||
|
|
||||||
# list for sightseeing
|
# list for sightseeing
|
||||||
if preferences.sightseeing.score != 0:
|
if preferences.sightseeing.score != 0:
|
||||||
|
self.logger.debug('Fetching sightseeing landmarks...')
|
||||||
score_function = lambda score: score * 10 * preferences.sightseeing.score / 5
|
score_function = lambda score: score * 10 * preferences.sightseeing.score / 5
|
||||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
|
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
|
||||||
all_landmarks.update(current_landmarks)
|
all_landmarks.update(current_landmarks)
|
||||||
|
self.logger.debug('Fetching sightseeing clusters...')
|
||||||
|
|
||||||
# special pipeline for historic neighborhoods
|
# special pipeline for historic neighborhoods
|
||||||
neighborhood_manager = ClusterManager(bbox, 'sightseeing')
|
neighborhood_manager = ClusterManager(bbox, 'sightseeing')
|
||||||
historic_clusters = neighborhood_manager.generate_clusters()
|
historic_clusters = neighborhood_manager.generate_clusters()
|
||||||
all_landmarks.update(historic_clusters)
|
all_landmarks.update(historic_clusters)
|
||||||
|
self.logger.debug('Sightseeing clusters done')
|
||||||
|
|
||||||
# list for nature
|
# list for nature
|
||||||
if preferences.nature.score != 0:
|
if preferences.nature.score != 0:
|
||||||
|
self.logger.debug('Fetching nature landmarks...')
|
||||||
score_function = lambda score: score * 10 * self.nature_coeff * preferences.nature.score / 5
|
score_function = lambda score: score * 10 * self.nature_coeff * preferences.nature.score / 5
|
||||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
|
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
|
||||||
all_landmarks.update(current_landmarks)
|
all_landmarks.update(current_landmarks)
|
||||||
|
|
||||||
|
|
||||||
# list for shopping
|
# list for shopping
|
||||||
if preferences.shopping.score != 0:
|
if preferences.shopping.score != 0:
|
||||||
|
self.logger.debug('Fetching shopping landmarks...')
|
||||||
score_function = lambda score: score * 10 * preferences.shopping.score / 5
|
score_function = lambda score: score * 10 * preferences.shopping.score / 5
|
||||||
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
|
current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
|
||||||
|
self.logger.debug('Fetching shopping clusters...')
|
||||||
|
|
||||||
# set time for all shopping activites :
|
# set time for all shopping activites :
|
||||||
for landmark in current_landmarks : landmark.duration = 30
|
for landmark in current_landmarks : landmark.duration = 30
|
||||||
@ -111,18 +120,19 @@ class LandmarkManager:
|
|||||||
shopping_manager = ClusterManager(bbox, 'shopping')
|
shopping_manager = ClusterManager(bbox, 'shopping')
|
||||||
shopping_clusters = shopping_manager.generate_clusters()
|
shopping_clusters = shopping_manager.generate_clusters()
|
||||||
all_landmarks.update(shopping_clusters)
|
all_landmarks.update(shopping_clusters)
|
||||||
|
self.logger.debug('Shopping clusters done')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
landmarks_constrained = take_most_important(all_landmarks, self.N_important)
|
landmarks_constrained = take_most_important(all_landmarks, self.N_important)
|
||||||
self.logger.info(f'Generated {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
|
self.logger.info(f'All landmarks generated : {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
|
||||||
|
|
||||||
return all_landmarks, landmarks_constrained
|
return all_landmarks, landmarks_constrained
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
|
|
||||||
"""
|
"""
|
||||||
|
def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
|
||||||
|
|
||||||
Count the number of OpenStreetMap elements (nodes, ways, relations) within a specified radius of the given location.
|
Count the number of OpenStreetMap elements (nodes, ways, relations) within a specified radius of the given location.
|
||||||
|
|
||||||
This function constructs a bounding box around the specified coordinates based on the radius. It then queries
|
This function constructs a bounding box around the specified coordinates based on the radius. It then queries
|
||||||
@ -134,7 +144,7 @@ class LandmarkManager:
|
|||||||
Returns:
|
Returns:
|
||||||
int: The number of elements (nodes, ways, relations) within the specified radius. Returns 0 if no elements
|
int: The number of elements (nodes, ways, relations) within the specified radius. Returns 0 if no elements
|
||||||
are found or if an error occurs during the query.
|
are found or if an error occurs during the query.
|
||||||
"""
|
|
||||||
|
|
||||||
lat = coordinates[0]
|
lat = coordinates[0]
|
||||||
lon = coordinates[1]
|
lon = coordinates[1]
|
||||||
@ -162,6 +172,7 @@ class LandmarkManager:
|
|||||||
return N_elem
|
return N_elem
|
||||||
except:
|
except:
|
||||||
return 0
|
return 0
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
|
# def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
|
||||||
@ -211,7 +222,7 @@ class LandmarkManager:
|
|||||||
# caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously
|
# caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously
|
||||||
# we need to split the selectors into separate queries and merge the results
|
# we need to split the selectors into separate queries and merge the results
|
||||||
for sel in dict_to_selector_list(amenity_selector):
|
for sel in dict_to_selector_list(amenity_selector):
|
||||||
self.logger.debug(f"Current selector: {sel}")
|
# self.logger.debug(f"Current selector: {sel}")
|
||||||
|
|
||||||
element_types = ['way', 'relation']
|
element_types = ['way', 'relation']
|
||||||
|
|
||||||
@ -230,7 +241,7 @@ class LandmarkManager:
|
|||||||
includeCenter = True,
|
includeCenter = True,
|
||||||
out = 'center'
|
out = 'center'
|
||||||
)
|
)
|
||||||
self.logger.debug(f"Query: {query}")
|
# self.logger.debug(f"Query: {query}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.overpass.query(query)
|
result = self.overpass.query(query)
|
||||||
|
@ -35,11 +35,7 @@ class Optimizer:
|
|||||||
"""
|
"""
|
||||||
Initialize the objective function coefficients and inequality constraints.
|
Initialize the objective function coefficients and inequality constraints.
|
||||||
-> Adds 1 row of constraints
|
-> Adds 1 row of constraints
|
||||||
|
-> Pre-allocates A_ub for the rest of the computations with 2*L rows
|
||||||
1 row
|
|
||||||
+ L-1 rows
|
|
||||||
|
|
||||||
-> Pre-allocates A_ub for the rest of the computations
|
|
||||||
|
|
||||||
This function computes the distances between all landmarks and stores
|
This function computes the distances between all landmarks and stores
|
||||||
their attractiveness to maximize sightseeing. The goal is to maximize
|
their attractiveness to maximize sightseeing. The goal is to maximize
|
||||||
@ -59,36 +55,42 @@ class Optimizer:
|
|||||||
c = np.zeros(L, dtype=np.int16)
|
c = np.zeros(L, dtype=np.int16)
|
||||||
|
|
||||||
# Coefficients of inequality constraints (left-hand side)
|
# Coefficients of inequality constraints (left-hand side)
|
||||||
A_first = np.zeros((L, L), dtype=np.int16)
|
A_ub = np.zeros((2*L, L*L), dtype=np.int16)
|
||||||
|
b_ub = np.zeros(2*L, dtype=np.int16)
|
||||||
|
|
||||||
|
# Fill in first row
|
||||||
|
b_ub[0] = round(max_time*self.overshoot)
|
||||||
|
|
||||||
for i, spot1 in enumerate(landmarks) :
|
for i, spot1 in enumerate(landmarks) :
|
||||||
c[i] = -spot1.attractiveness
|
c[i] = -spot1.attractiveness
|
||||||
for j in range(i+1, L) :
|
for j in range(i+1, L) :
|
||||||
if i !=j :
|
if i !=j :
|
||||||
t = get_time(spot1.location, landmarks[j].location) + spot1.duration
|
t = get_time(spot1.location, landmarks[j].location) + spot1.duration
|
||||||
A_first[i,j] = t
|
A_ub[0, i*L + j] = t
|
||||||
A_first[j,i] = t
|
A_ub[0, j*L + i] = t
|
||||||
|
|
||||||
|
# Expand 'c' to L*L for every decision variable
|
||||||
|
c = np.tile(c, L)
|
||||||
|
|
||||||
# Now sort and modify A_ub for each row
|
# Now sort and modify A_ub for each row
|
||||||
if L > 22 :
|
if L > 22 :
|
||||||
for i in range(L):
|
for i in range(L):
|
||||||
# Get indices of the 20 smallest values in row i
|
# Get indices of the 4 smallest values in row i
|
||||||
closest_indices = np.argpartition(A_first[i, :], 20)[:20]
|
row_values = A_ub[0, i*L:i*L+L]
|
||||||
|
closest_indices = np.argpartition(row_values, 22)[:22]
|
||||||
|
|
||||||
# Create a mask for non-closest landmarks
|
# Create a mask for non-closest landmarks
|
||||||
mask = np.ones(L, dtype=bool)
|
mask = np.ones(L, dtype=bool)
|
||||||
mask[closest_indices] = False
|
mask[closest_indices] = False
|
||||||
|
|
||||||
# Set non-closest landmarks to 32700
|
# Set non-closest landmarks to 32765
|
||||||
A_first[i, mask] = 32765
|
row_values[mask] = 32765
|
||||||
|
A_ub[0, i*L:i*L+L] = row_values
|
||||||
|
|
||||||
# Replicate the objective function 'c' for each decision variable (L times)
|
return c, A_ub, b_ub
|
||||||
c = np.tile(c, L) # This correctly expands 'c' to L*L
|
|
||||||
|
|
||||||
return c, A_first.flatten(), [max_time*self.overshoot]
|
|
||||||
|
|
||||||
|
|
||||||
def respect_number(self, L, max_landmarks: int):
|
def respect_number(self, A, b, L, max_landmarks: int):
|
||||||
"""
|
"""
|
||||||
Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks.
|
Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks.
|
||||||
-> Adds L-1 rows of constraints
|
-> Adds L-1 rows of constraints
|
||||||
@ -100,24 +102,25 @@ class Optimizer:
|
|||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
"""
|
"""
|
||||||
# First constraint: each landmark is visited exactly once
|
# First constraint: each landmark is visited exactly once
|
||||||
A = np.zeros((L-1, L*L), dtype=np.int8)
|
# A = np.zeros((L-1, L*L), dtype=np.int8)
|
||||||
b = []
|
# b = []
|
||||||
|
# Fill-in row 2 until row L-2
|
||||||
for i in range(1, L-1):
|
for i in range(1, L-1):
|
||||||
A[i-1, L*i:L*(i+1)] = np.ones(L, dtype=np.int8)
|
A[i, L*i:L*(i+1)] = np.ones(L, dtype=np.int16)
|
||||||
b.append(1)
|
b[i] = 1
|
||||||
|
|
||||||
|
# Fill-in row L-1
|
||||||
# Second constraint: cap the total number of visits
|
# Second constraint: cap the total number of visits
|
||||||
A[-1, :] = np.ones(L*L, dtype=np.int8)
|
A[L-1, :] = np.ones(L*L, dtype=np.int16)
|
||||||
b.append(max_landmarks+2)
|
b[L-1] = max_landmarks+2
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
def break_sym(self, L):
|
def break_sym(self, A, b, L):
|
||||||
"""
|
"""
|
||||||
Generate constraints to prevent simultaneous travel between two landmarks
|
Generate constraints to prevent simultaneous travel between two landmarks
|
||||||
in both directions. Constraint to not have d14 and d41 simultaneously.
|
in both directions. Constraint to not have d14 and d41 simultaneously.
|
||||||
Does not prevent cyclic paths with more elements
|
Does not prevent cyclic paths with more elements
|
||||||
-> Adds a variable number of rows of constraints
|
-> Adds L rows of constraints (some of which might be zero)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
L (int): Number of landmarks.
|
L (int): Number of landmarks.
|
||||||
@ -126,25 +129,27 @@ class Optimizer:
|
|||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and
|
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and
|
||||||
the right-hand side of the inequality constraints.
|
the right-hand side of the inequality constraints.
|
||||||
"""
|
"""
|
||||||
b = []
|
# b = []
|
||||||
upper_ind = np.triu_indices(L,0,L)
|
upper_ind = np.triu_indices(L,0,L)
|
||||||
up_ind_x = upper_ind[0]
|
up_ind_x = upper_ind[0]
|
||||||
up_ind_y = upper_ind[1]
|
up_ind_y = upper_ind[1]
|
||||||
|
|
||||||
A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8)
|
# A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8)
|
||||||
for i, _ in enumerate(up_ind_x[1:]) :
|
# Fill-in rows L to 2*L-1
|
||||||
|
for i in range(L) :
|
||||||
if up_ind_x[i] != up_ind_y[i] :
|
if up_ind_x[i] != up_ind_y[i] :
|
||||||
A[i, up_ind_x[i]*L + up_ind_y[i]] = 1
|
A[L+i, up_ind_x[i]*L + up_ind_y[i]] = 1
|
||||||
A[i, up_ind_y[i]*L + up_ind_x[i]] = 1
|
A[L+i, up_ind_y[i]*L + up_ind_x[i]] = 1
|
||||||
b.append(1)
|
b[L+i] = 1
|
||||||
|
|
||||||
return A[~np.all(A == 0, axis=1)], b
|
# return A[~np.all(A == 0, axis=1)], b
|
||||||
|
|
||||||
|
|
||||||
def init_eq_not_stay(self, L: int):
|
def init_eq_not_stay(self, landmarks: list):
|
||||||
"""
|
"""
|
||||||
Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.).
|
Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.).
|
||||||
-> Adds 1 row of constraints
|
-> Adds 1 row of constraints
|
||||||
|
-> Pre-allocates A_eq for the rest of the computations with (L+2 + dynamic incr) rows
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
L (int): Number of landmarks.
|
L (int): Number of landmarks.
|
||||||
@ -152,15 +157,78 @@ class Optimizer:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints.
|
tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints.
|
||||||
"""
|
"""
|
||||||
|
L = len(landmarks)
|
||||||
|
incr = 0
|
||||||
|
for i, elem in enumerate(landmarks) :
|
||||||
|
if (elem.must_do or elem.must_avoid) and i not in [0, L-1]:
|
||||||
|
incr += 1
|
||||||
|
|
||||||
|
A_eq = np.zeros((L+2+incr, L*L), dtype=np.int8)
|
||||||
|
b_eq = np.zeros(L+2+incr, dtype=np.int8)
|
||||||
l = np.zeros((L, L), dtype=np.int8)
|
l = np.zeros((L, L), dtype=np.int8)
|
||||||
|
|
||||||
# Set diagonal elements to 1 (to prevent staying in the same position)
|
# Set diagonal elements to 1 (to prevent staying in the same position)
|
||||||
np.fill_diagonal(l, 1)
|
np.fill_diagonal(l, 1)
|
||||||
|
|
||||||
return l.flatten(), [0]
|
# Fill-in first row
|
||||||
|
A_eq[0,:] = l.flatten()
|
||||||
|
b_eq[0] = 0
|
||||||
|
|
||||||
|
return A_eq, b_eq
|
||||||
|
|
||||||
|
|
||||||
def respect_user_must_do(self, landmarks: list[Landmark]) :
|
# Constraint to ensure start at start and finish at goal
|
||||||
|
def respect_start_finish(self, A, b, L: int):
|
||||||
|
"""
|
||||||
|
Generate constraints to ensure that the optimization starts at the designated
|
||||||
|
start landmark and finishes at the goal landmark.
|
||||||
|
-> Adds 3 rows of constraints
|
||||||
|
|
||||||
|
Args:
|
||||||
|
L (int): Number of landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
|
"""
|
||||||
|
# Fill-in row 1.
|
||||||
|
A[1, :L] = np.ones(L, dtype=np.int8) # sets departures only for start (horizontal ones)
|
||||||
|
for k in range(L-1) :
|
||||||
|
if k != 0 :
|
||||||
|
# Fill-in row 2
|
||||||
|
A[2, k*L+L-1] = 1 # sets arrivals only for finish (vertical ones)
|
||||||
|
# Fill-in row 3
|
||||||
|
A[3, k*L] = 1
|
||||||
|
|
||||||
|
A[3, L*(L-1):] = np.ones(L, dtype=np.int8) # prevents arrivals at start and departures from goal
|
||||||
|
b[1:4] = [1, 1, 0]
|
||||||
|
|
||||||
|
# return A, b
|
||||||
|
|
||||||
|
|
||||||
|
def respect_order(self, A, b, L: int):
|
||||||
|
"""
|
||||||
|
Generate constraints to tie the optimization problem together and prevent
|
||||||
|
stacked ones, although this does not fully prevent circles.
|
||||||
|
-> Adds L-2 rows of constraints
|
||||||
|
|
||||||
|
Args:
|
||||||
|
L (int): Number of landmarks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
|
"""
|
||||||
|
ones = np.ones(L, dtype=np.int8)
|
||||||
|
|
||||||
|
# Fill-in rows 4 to L+2
|
||||||
|
for i in range(1, L-1) : # Prevent stacked ones
|
||||||
|
for j in range(L) :
|
||||||
|
A[i-1+4, i + j*L] = -1
|
||||||
|
A[i-1+4, i*L:(i+1)*L] = ones
|
||||||
|
|
||||||
|
b[4:L+2] = np.zeros(L-2, dtype=np.int8)
|
||||||
|
|
||||||
|
|
||||||
|
def respect_user_must(self, A, b, landmarks: list[Landmark]) :
|
||||||
"""
|
"""
|
||||||
Generate constraints to ensure that landmarks marked as 'must_do' are included in the optimization.
|
Generate constraints to ensure that landmarks marked as 'must_do' are included in the optimization.
|
||||||
-> Adds a variable number of rows of constraints BUT CAN BE PRE COMPUTED
|
-> Adds a variable number of rows of constraints BUT CAN BE PRE COMPUTED
|
||||||
@ -173,88 +241,20 @@ class Optimizer:
|
|||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
"""
|
"""
|
||||||
L = len(landmarks)
|
L = len(landmarks)
|
||||||
A = np.zeros((L, L*L), dtype=np.int8)
|
ones = np.ones(L, dtype=np.int8)
|
||||||
b = []
|
incr = 0
|
||||||
|
|
||||||
for i, elem in enumerate(landmarks) :
|
|
||||||
if elem.must_do is True and elem.name not in ['finish', 'start']:
|
|
||||||
A[i, i*L:i*L+L] = np.ones(L, dtype=np.int8)
|
|
||||||
b.append(1)
|
|
||||||
|
|
||||||
return A[~np.all(A == 0, axis=1)], b
|
|
||||||
|
|
||||||
|
|
||||||
def respect_user_must_avoid(self, landmarks: list[Landmark]) :
|
|
||||||
"""
|
|
||||||
Generate constraints to ensure that landmarks marked as 'must_avoid' are skipped
|
|
||||||
in the optimization.
|
|
||||||
-> Adds a variable number of rows of constraints BUT CAN BE PRE COMPUTED
|
|
||||||
|
|
||||||
Args:
|
|
||||||
landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_avoid'.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
|
||||||
"""
|
|
||||||
L = len(landmarks)
|
|
||||||
A = np.zeros((L, L*L), dtype=np.int8)
|
|
||||||
b = []
|
|
||||||
|
|
||||||
for i, elem in enumerate(landmarks) :
|
for i, elem in enumerate(landmarks) :
|
||||||
if elem.must_do is True and i not in [0, L-1]:
|
if elem.must_do is True and i not in [0, L-1]:
|
||||||
A[i, i*L:i*L+L] = np.ones(L, dtype=np.int8)
|
# First part of the dynamic infill
|
||||||
b.append(0)
|
A[L+2+incr, i*L:i*L+L] = ones
|
||||||
|
b[L+2+incr] = 1
|
||||||
return A[~np.all(A == 0, axis=1)], b
|
incr += 1
|
||||||
|
if elem.must_avoid is True and i not in [0, L-1]:
|
||||||
|
# Second part of the dynamic infill
|
||||||
# Constraint to ensure start at start and finish at goal
|
A[L+2+incr, i*L:i*L+L] = ones
|
||||||
def respect_start_finish(self, L: int):
|
b[L+2+incr] = 0
|
||||||
"""
|
incr += 1
|
||||||
Generate constraints to ensure that the optimization starts at the designated
|
|
||||||
start landmark and finishes at the goal landmark.
|
|
||||||
-> Adds 3 rows of constraints
|
|
||||||
Args:
|
|
||||||
L (int): Number of landmarks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
A = np.zeros((3, L*L), dtype=np.int8)
|
|
||||||
|
|
||||||
A[0, :L] = np.ones(L, dtype=np.int8) # sets departures only for start (horizontal ones)
|
|
||||||
for k in range(L-1) :
|
|
||||||
A[2, k*L] = 1
|
|
||||||
if k != 0 :
|
|
||||||
A[1, k*L+L-1] = 1 # sets arrivals only for finish (vertical ones)
|
|
||||||
A[2, L*(L-1):] = np.ones(L, dtype=np.int8) # prevents arrivals at start and departures from goal
|
|
||||||
b = [1, 1, 0]
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
def respect_order(self, L: int):
|
|
||||||
"""
|
|
||||||
Generate constraints to tie the optimization problem together and prevent
|
|
||||||
stacked ones, although this does not fully prevent circles.
|
|
||||||
-> Adds L-2 rows of constraints
|
|
||||||
|
|
||||||
Args:
|
|
||||||
L (int): Number of landmarks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
A = np.zeros((L-2, L*L), dtype=np.int8)
|
|
||||||
b = [0]*(L-2)
|
|
||||||
for i in range(1, L-1) : # Prevent stacked ones
|
|
||||||
for j in range(L) :
|
|
||||||
A[i-1, i + j*L] = -1
|
|
||||||
A[i-1, i*L:(i+1)*L] = np.ones(L, dtype=np.int8)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
# Prevent the use of a particular solution
|
# Prevent the use of a particular solution
|
||||||
@ -282,13 +282,14 @@ class Optimizer:
|
|||||||
vertices_visited = ind_a
|
vertices_visited = ind_a
|
||||||
vertices_visited.remove(0)
|
vertices_visited.remove(0)
|
||||||
|
|
||||||
ones = [1]*L
|
ones = np.ones(L, dtype=np.int8)
|
||||||
h = [0]*N
|
h = np.zeros(L*L, dtype=np.int8)
|
||||||
|
|
||||||
for i in range(L) :
|
for i in range(L) :
|
||||||
if i in vertices_visited :
|
if i in vertices_visited :
|
||||||
h[i*L:i*L+L] = ones
|
h[i*L:i*L+L] = ones
|
||||||
|
|
||||||
return h, [len(vertices_visited)-1]
|
return h, np.array([len(vertices_visited)-1])
|
||||||
|
|
||||||
|
|
||||||
# Prevents the creation of the same circle (both directions)
|
# Prevents the creation of the same circle (both directions)
|
||||||
@ -303,22 +304,21 @@ class Optimizer:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
||||||
"""
|
"""
|
||||||
|
l = np.zeros((2, L*L), dtype=np.int8)
|
||||||
|
|
||||||
l1 = [0]*L*L
|
|
||||||
l2 = [0]*L*L
|
|
||||||
for i, node in enumerate(circle_vertices[:-1]) :
|
for i, node in enumerate(circle_vertices[:-1]) :
|
||||||
next = circle_vertices[i+1]
|
next = circle_vertices[i+1]
|
||||||
|
|
||||||
l1[node*L + next] = 1
|
l[0, node*L + next] = 1
|
||||||
l2[next*L + node] = 1
|
l[1, next*L + node] = 1
|
||||||
|
|
||||||
s = circle_vertices[0]
|
s = circle_vertices[0]
|
||||||
g = circle_vertices[-1]
|
g = circle_vertices[-1]
|
||||||
|
|
||||||
l1[g*L + s] = 1
|
l[0, g*L + s] = 1
|
||||||
l2[s*L + g] = 1
|
l[1, s*L + g] = 1
|
||||||
|
|
||||||
return np.vstack((l1, l2)), [0, 0]
|
return l, np.zeros(2, dtype=np.int8)
|
||||||
|
|
||||||
|
|
||||||
def is_connected(self, resx) :
|
def is_connected(self, resx) :
|
||||||
@ -331,10 +331,7 @@ class Optimizer:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles.
|
tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles.
|
||||||
"""
|
"""
|
||||||
|
resx = np.round(resx).astype(np.int8) # round all elements and cast them to int
|
||||||
# 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
|
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.
|
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||||
@ -342,16 +339,53 @@ class Optimizer:
|
|||||||
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
||||||
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||||
|
|
||||||
ind_a = nonzero_tup[0].tolist()
|
ind_a = nonzero_tup[0] # removed .tolist()
|
||||||
ind_b = nonzero_tup[1].tolist()
|
ind_b = nonzero_tup[1]
|
||||||
|
|
||||||
# Step 1: Create a graph representation
|
# Extract all journeys
|
||||||
|
all_journeys_nodes = []
|
||||||
|
visited_nodes = set()
|
||||||
|
|
||||||
|
for node in ind_a:
|
||||||
|
if node not in visited_nodes:
|
||||||
|
journey_nodes = self.get_journey(node, ind_a, ind_b)
|
||||||
|
all_journeys_nodes.append(journey_nodes)
|
||||||
|
visited_nodes.update(journey_nodes)
|
||||||
|
|
||||||
|
for l in all_journeys_nodes :
|
||||||
|
if 0 in l :
|
||||||
|
all_journeys_nodes.remove(l)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not all_journeys_nodes :
|
||||||
|
return None
|
||||||
|
|
||||||
|
return all_journeys_nodes
|
||||||
|
|
||||||
|
|
||||||
|
def get_journey(self, start, ind_a, ind_b):
|
||||||
|
"""
|
||||||
|
Trace the journey starting from a given node and follow the connections between landmarks.
|
||||||
|
This method constructs a graph from two lists of landmark connections, `ind_a` and `ind_b`,
|
||||||
|
where each element in `ind_a` is connected to the corresponding element in `ind_b`.
|
||||||
|
It then performs a depth-first search (DFS) starting from the `start` node to determine
|
||||||
|
the path (journey) by following the connections.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start (int): The starting node of the journey.
|
||||||
|
ind_a (list[int]): List of "from" nodes, representing the starting points of each connection.
|
||||||
|
ind_b (list[int]): List of "to" nodes, representing the endpoints of each connection.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: A list of nodes representing the order of the journey, starting from the `start` node.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
If `ind_a = [0, 1, 2]` and `ind_b = [1, 2, 3]`, starting from node 0, the journey would be `[0, 1, 2, 3]`.
|
||||||
|
"""
|
||||||
graph = defaultdict(list)
|
graph = defaultdict(list)
|
||||||
for a, b in zip(ind_a, ind_b):
|
for a, b in zip(ind_a, ind_b):
|
||||||
graph[a].append(b)
|
graph[a].append(b)
|
||||||
|
|
||||||
# Step 2: Function to perform BFS/DFS to extract journeys
|
|
||||||
def get_journey(start):
|
|
||||||
journey_nodes = []
|
journey_nodes = []
|
||||||
visited = set()
|
visited = set()
|
||||||
stack = deque([start])
|
stack = deque([start])
|
||||||
@ -367,26 +401,56 @@ class Optimizer:
|
|||||||
|
|
||||||
return journey_nodes
|
return journey_nodes
|
||||||
|
|
||||||
# Step 3: Extract all journeys
|
|
||||||
all_journeys_nodes = []
|
|
||||||
visited_nodes = set()
|
|
||||||
|
|
||||||
for node in ind_a:
|
def get_order(self, resx):
|
||||||
if node not in visited_nodes:
|
"""
|
||||||
journey_nodes = get_journey(node)
|
Determine the order of visits given the result of the optimization.
|
||||||
all_journeys_nodes.append(journey_nodes)
|
|
||||||
visited_nodes.update(journey_nodes)
|
|
||||||
|
|
||||||
for l in all_journeys_nodes :
|
Args:
|
||||||
if 0 in l :
|
resx (list): List of edge weights.
|
||||||
order = l
|
|
||||||
all_journeys_nodes.remove(l)
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(all_journeys_nodes) == 0 :
|
Returns:
|
||||||
return order, None
|
list[int]: A list containing the visit order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# first round the results to have only 0-1 values
|
||||||
|
resx = np.round(resx).astype(np.uint8) # round all elements and cast them to int
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
order = [0]
|
||||||
|
current = 0
|
||||||
|
used_indices = set() # Track visited index pairs
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Find index of the current node in ind_a
|
||||||
|
try:
|
||||||
|
i = ind_a.index(current)
|
||||||
|
except ValueError:
|
||||||
|
break # No more links, stop the search
|
||||||
|
|
||||||
|
if i in used_indices:
|
||||||
|
break # Prevent infinite loops
|
||||||
|
|
||||||
|
used_indices.add(i) # Mark this index as visited
|
||||||
|
next_node = ind_b[i] # Get the corresponding node in ind_b
|
||||||
|
order.append(next_node) # Add it to the path
|
||||||
|
|
||||||
|
# Switch roles, now look for next_node in ind_a
|
||||||
|
try:
|
||||||
|
current = next_node
|
||||||
|
except ValueError:
|
||||||
|
break # No further connections, end the path
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
return order, all_journeys_nodes
|
|
||||||
|
|
||||||
|
|
||||||
def link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] :
|
def link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] :
|
||||||
@ -449,33 +513,34 @@ class Optimizer:
|
|||||||
L = len(landmarks)
|
L = len(landmarks)
|
||||||
|
|
||||||
# SET CONSTRAINTS FOR INEQUALITY
|
# SET CONSTRAINTS FOR INEQUALITY
|
||||||
c, A_ub, b_ub = self.init_ub_time(landmarks, max_time) # Add the distances from each landmark to the other
|
c, A_ub, b_ub = self.init_ub_time(landmarks, max_time) # Adds the distances from each landmark to the other.
|
||||||
|
self.respect_number(A_ub, b_ub, L, max_landmarks) # Respects 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).
|
self.break_sym(A_ub, b_ub, L) # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces.
|
||||||
A_ub = np.vstack((A_ub, A))
|
|
||||||
b_ub += b
|
|
||||||
|
|
||||||
A, b = self.break_sym(L) # break the 'zig-zag' symmetry
|
|
||||||
A_ub = np.vstack((A_ub, A))
|
|
||||||
b_ub += b
|
|
||||||
|
|
||||||
|
|
||||||
# SET CONSTRAINTS FOR EQUALITY
|
# SET CONSTRAINTS FOR EQUALITY
|
||||||
A_eq, b_eq = self.init_eq_not_stay(L) # Force solution not to stay in same place
|
A_eq, b_eq = self.init_eq_not_stay(landmarks) # Force solution not to stay in same place
|
||||||
A, b = self.respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
|
self.respect_start_finish(A_eq, b_eq, L) # Force start and finish positions
|
||||||
if len(b) > 0 :
|
self.respect_order(A_eq, b_eq, L) # Respect order of visit (only works when max_time is limiting factor)
|
||||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
self.respect_user_must(A_eq, b_eq, landmarks) # Force to do/avoid landmarks set by user.
|
||||||
b_eq += b
|
|
||||||
A, b = self.respect_user_must_avoid(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
|
self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.")
|
||||||
if len(b) > 0 :
|
|
||||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
|
||||||
b_eq += b
|
|
||||||
A, b = self.respect_start_finish(L) # Force start and finish positions
|
# A, b = self.respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
|
||||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
# if len(b) > 0 :
|
||||||
b_eq += b
|
# A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||||
A, b = self.respect_order(L) # Respect order of visit (only works when max_time is limiting factor)
|
# b_eq += b
|
||||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
# A, b = self.respect_user_must_avoid(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal
|
||||||
b_eq += b
|
# if len(b) > 0 :
|
||||||
|
# A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||||
|
# b_eq += b
|
||||||
|
# A, b = self.respect_start_finish(L) # Force start and finish positions
|
||||||
|
# A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||||
|
# b_eq += b
|
||||||
|
# A, b = self.respect_order(L) # Respect order of visit (only works when max_time is limiting factor)
|
||||||
|
# A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
||||||
|
# b_eq += b
|
||||||
# until here opti
|
# until here opti
|
||||||
|
|
||||||
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
|
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
|
||||||
@ -484,38 +549,46 @@ class Optimizer:
|
|||||||
# Solve linear programming problem
|
# 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)
|
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)
|
||||||
|
|
||||||
|
self.logger.debug("First results are out. Looking out for circles and correcting.")
|
||||||
|
|
||||||
# Raise error if no solution is found. FIXME: for now this throws the internal server error
|
# Raise error if no solution is found. FIXME: for now this throws the internal server error
|
||||||
if not res.success :
|
if not res.success :
|
||||||
|
self.logger.error("The problem is overconstrained, no solution on first try.")
|
||||||
raise ArithmeticError("No solution could be found, the problem is overconstrained. Try with a longer trip (>30 minutes).")
|
raise ArithmeticError("No solution could be found, the problem is overconstrained. Try with a longer trip (>30 minutes).")
|
||||||
|
|
||||||
# If there is a solution, we're good to go, just check for connectiveness
|
# If there is a solution, we're good to go, just check for connectiveness
|
||||||
order, circles = self.is_connected(res.x)
|
circles = self.is_connected(res.x)
|
||||||
#nodes, edges = is_connected(res.x)
|
#nodes, edges = is_connected(res.x)
|
||||||
i = 0
|
i = 0
|
||||||
timeout = 80
|
timeout = 80
|
||||||
while circles is not None and i < timeout:
|
while circles is not None and i < timeout:
|
||||||
|
i += 1
|
||||||
|
# print(f"Iteration {i} of fixing circles")
|
||||||
A, b = self.prevent_config(res.x)
|
A, b = self.prevent_config(res.x)
|
||||||
A_ub = np.vstack((A_ub, A))
|
A_ub = np.vstack((A_ub, A))
|
||||||
b_ub += b
|
b_ub = np.concatenate((b_ub, b))
|
||||||
#A_ub, b_ub = prevent_circle(order, len(landmarks), A_ub, b_ub)
|
|
||||||
for circle in circles :
|
for circle in circles :
|
||||||
A, b = self.prevent_circle(circle, L)
|
A, b = self.prevent_circle(circle, L)
|
||||||
A_eq = np.vstack((A_eq, A))
|
A_eq = np.vstack((A_eq, A))
|
||||||
b_eq += b
|
b_eq = np.concatenate((b_eq, b))
|
||||||
|
|
||||||
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)
|
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)
|
||||||
|
|
||||||
if not res.success :
|
if not res.success :
|
||||||
|
self.logger.error(f'Unexpected error after {timeout} iterations of fixing circles.')
|
||||||
raise ArithmeticError("Solving failed because of overconstrained problem")
|
raise ArithmeticError("Solving failed because of overconstrained problem")
|
||||||
order, circles = self.is_connected(res.x)
|
circles = self.is_connected(res.x)
|
||||||
#nodes, edges = is_connected(res.x)
|
#nodes, edges = is_connected(res.x)
|
||||||
if circles is None :
|
if circles is None :
|
||||||
break
|
break
|
||||||
# print(i)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if i == timeout :
|
if i == timeout :
|
||||||
|
self.logger.error(f'Timeout: No solution found after {timeout} iterations.')
|
||||||
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
||||||
|
|
||||||
#sort the landmarks in the order of the solution
|
# Sort the landmarks in the order of the solution
|
||||||
|
order = self.get_order(res.x)
|
||||||
tour = [landmarks[i] for i in order]
|
tour = [landmarks[i] for i in order]
|
||||||
|
|
||||||
self.logger.debug(f"Re-optimized {i} times, score: {int(-res.fun)}")
|
self.logger.debug(f"Re-optimized {i} times, score: {int(-res.fun)}")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user