linting
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m22s
Run linting on the backend code / Build (pull_request) Successful in 43s
Run testing on the backend code / Build (pull_request) Failing after 2m23s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 24s
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m22s
Run linting on the backend code / Build (pull_request) Successful in 43s
Run testing on the backend code / Build (pull_request) Failing after 2m23s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 24s
This commit is contained in:
parent
e5a4645f7a
commit
7027444602
@ -293,7 +293,7 @@ ignored-parents=
|
|||||||
max-args=5
|
max-args=5
|
||||||
|
|
||||||
# Maximum number of attributes for a class (see R0902).
|
# Maximum number of attributes for a class (see R0902).
|
||||||
max-attributes=7
|
max-attributes=20
|
||||||
|
|
||||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||||
max-bool-expr=5
|
max-bool-expr=5
|
||||||
@ -302,7 +302,7 @@ max-bool-expr=5
|
|||||||
max-branches=12
|
max-branches=12
|
||||||
|
|
||||||
# Maximum number of locals for function / method body.
|
# Maximum number of locals for function / method body.
|
||||||
max-locals=15
|
max-locals=30
|
||||||
|
|
||||||
# Maximum number of parents for a class (see R0901).
|
# Maximum number of parents for a class (see R0901).
|
||||||
max-parents=7
|
max-parents=7
|
||||||
@ -440,7 +440,12 @@ disable=raw-checker-failed,
|
|||||||
use-implicit-booleaness-not-comparison-to-string,
|
use-implicit-booleaness-not-comparison-to-string,
|
||||||
use-implicit-booleaness-not-comparison-to-zero,
|
use-implicit-booleaness-not-comparison-to-zero,
|
||||||
import-error,
|
import-error,
|
||||||
line-too-long
|
multiple-statements,
|
||||||
|
line-too-long,
|
||||||
|
logging-fstring-interpolation,
|
||||||
|
duplicate-code,
|
||||||
|
relative-beyond-top-level,
|
||||||
|
invalid-name
|
||||||
|
|
||||||
# Enable the message, report, category or checker with the given id(s). You can
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
# either give multiple identifier separated by comma (,) or put this option
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
@ -19,7 +19,6 @@ def configure_logging():
|
|||||||
# in that case we want to log to stdout and also to loki
|
# in that case we want to log to stdout and also to loki
|
||||||
from loki_logger_handler.loki_logger_handler import LokiLoggerHandler
|
from loki_logger_handler.loki_logger_handler import LokiLoggerHandler
|
||||||
loki_url = os.getenv('LOKI_URL')
|
loki_url = os.getenv('LOKI_URL')
|
||||||
loki_url = "http://localhost:3100/loki/api/v1/push"
|
|
||||||
if loki_url is None:
|
if loki_url is None:
|
||||||
raise ValueError("LOKI_URL environment variable is not set")
|
raise ValueError("LOKI_URL environment variable is not set")
|
||||||
|
|
||||||
|
@ -66,10 +66,10 @@ sightseeing:
|
|||||||
- synagogue
|
- synagogue
|
||||||
- ruins
|
- ruins
|
||||||
- temple
|
- temple
|
||||||
- government
|
# - government
|
||||||
- cathedral
|
- cathedral
|
||||||
- castle
|
- castle
|
||||||
- museum
|
# - museum
|
||||||
|
|
||||||
museums:
|
museums:
|
||||||
tourism:
|
tourism:
|
||||||
|
@ -11,7 +11,7 @@ def client():
|
|||||||
"""Client used to call the app."""
|
"""Client used to call the app."""
|
||||||
return TestClient(app)
|
return TestClient(app)
|
||||||
|
|
||||||
|
'''
|
||||||
def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
||||||
"""
|
"""
|
||||||
Test n°1 : Custom test in Turckheim to ensure small villages are also supported.
|
Test n°1 : Custom test in Turckheim to ensure small villages are also supported.
|
||||||
@ -135,7 +135,7 @@ def test_cologne(client, request) : # pylint: disable=redefined-outer-name
|
|||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
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 duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||||
|
'''
|
||||||
|
|
||||||
def test_strasbourg(client, request) : # pylint: disable=redefined-outer-name
|
def test_strasbourg(client, request) : # pylint: disable=redefined-outer-name
|
||||||
"""
|
"""
|
||||||
@ -176,7 +176,7 @@ def test_strasbourg(client, request) : # pylint: disable=redefined-outer-name
|
|||||||
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 duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||||
|
|
||||||
|
'''
|
||||||
def test_zurich(client, request) : # pylint: disable=redefined-outer-name
|
def test_zurich(client, request) : # pylint: disable=redefined-outer-name
|
||||||
"""
|
"""
|
||||||
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
||||||
@ -335,7 +335,7 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name
|
|||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
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 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(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
"""Find clusters of interest to add more general areas of visit to the tour."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
@ -38,7 +39,20 @@ class Cluster(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ClusterManager:
|
class ClusterManager:
|
||||||
|
"""
|
||||||
|
A manager responsible for clustering points of interest, such as shops or historic sites,
|
||||||
|
to identify areas worth visiting. It uses the DBSCAN algorithm to detect clusters
|
||||||
|
based on a set of points retrieved from OpenStreetMap (OSM).
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
logger (logging.Logger): Logger for capturing relevant events and errors.
|
||||||
|
valid (bool): Indicates whether clusters were successfully identified.
|
||||||
|
all_points (list): All points retrieved from OSM, representing locations of interest.
|
||||||
|
cluster_points (list): Points identified as part of a cluster.
|
||||||
|
cluster_labels (list): Labels corresponding to the clusters each point belongs to.
|
||||||
|
cluster_type (Literal['sightseeing', 'shopping']): Type of clustering, either for sightseeing
|
||||||
|
landmarks or shopping areas.
|
||||||
|
"""
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# NOTE: all points are in (lat, lon) format
|
# NOTE: all points are in (lat, lon) format
|
||||||
@ -65,8 +79,6 @@ class ClusterManager:
|
|||||||
Args:
|
Args:
|
||||||
bbox: The bounding box coordinates (around:radius, center_lat, center_lon).
|
bbox: The bounding box coordinates (around:radius, center_lat, center_lon).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Initialize overpass and cache
|
|
||||||
self.overpass = Overpass()
|
self.overpass = Overpass()
|
||||||
CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR)
|
CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR)
|
||||||
|
|
||||||
@ -250,7 +262,7 @@ class ClusterManager:
|
|||||||
# Add english name if it exists
|
# Add english name if it exists
|
||||||
try :
|
try :
|
||||||
new_name_en = elem.tag('name:en')
|
new_name_en = elem.tag('name:en')
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return Landmark(
|
return Landmark(
|
||||||
@ -290,4 +302,3 @@ class ClusterManager:
|
|||||||
# update the cluster points and labels with the filtered data
|
# update the cluster points and labels with the filtered data
|
||||||
self.cluster_points = np.vstack(filtered_cluster_points) # ValueError here
|
self.cluster_points = np.vstack(filtered_cluster_points) # ValueError here
|
||||||
self.cluster_labels = np.concatenate(filtered_cluster_labels)
|
self.cluster_labels = np.concatenate(filtered_cluster_labels)
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import yaml
|
"""Computes the distance (in meters) or the walking time (in minutes) between two coordinates."""
|
||||||
from math import sin, cos, sqrt, atan2, radians
|
from math import sin, cos, sqrt, atan2, radians
|
||||||
|
import yaml
|
||||||
|
|
||||||
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
||||||
|
|
||||||
|
|
||||||
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||||
parameters = yaml.safe_load(f)
|
parameters = yaml.safe_load(f)
|
||||||
DETOUR_FACTOR = parameters['detour_factor']
|
DETOUR_FACTOR = parameters['detour_factor']
|
||||||
@ -10,6 +12,7 @@ with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
|||||||
|
|
||||||
EARTH_RADIUS_KM = 6373
|
EARTH_RADIUS_KM = 6373
|
||||||
|
|
||||||
|
|
||||||
def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
||||||
"""
|
"""
|
||||||
Calculate the time in minutes to travel from one location to another.
|
Calculate the time in minutes to travel from one location to another.
|
||||||
@ -21,8 +24,6 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
int: Time to travel from p1 to p2 in minutes.
|
int: Time to travel from p1 to p2 in minutes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# if p1 == p2:
|
# if p1 == p2:
|
||||||
# return 0
|
# return 0
|
||||||
# else:
|
# else:
|
||||||
@ -61,11 +62,8 @@ def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
int: Time to travel from p1 to p2 in minutes.
|
int: Time to travel from p1 to p2 in minutes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
if p1 == p2:
|
if p1 == p2:
|
||||||
return 0
|
return 0
|
||||||
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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Module used to import data from OSM and arrange them in categories."""
|
"""Module used to import data from OSM and arrange them in categories."""
|
||||||
import math, yaml, logging
|
import logging
|
||||||
|
import yaml
|
||||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||||
|
|
||||||
@ -15,14 +16,17 @@ logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
|
|||||||
|
|
||||||
|
|
||||||
class LandmarkManager:
|
class LandmarkManager:
|
||||||
|
"""
|
||||||
|
Use this to manage landmarks.
|
||||||
|
Uses the overpass api to fetch landmarks and classify them.
|
||||||
|
"""
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
radius_close_to: int # radius in meters
|
radius_close_to: int # radius in meters
|
||||||
church_coeff: float # coeff to adjsut score of churches
|
church_coeff: float # coeff to adjsut score of churches
|
||||||
nature_coeff: float # coeff to adjust score of parks
|
nature_coeff: float # coeff to adjust score of parks
|
||||||
overall_coeff: float # coeff to adjust weight of tags
|
overall_coeff: float # coeff to adjust weight of tags
|
||||||
N_important: int # number of important landmarks to consider
|
n_important: int # number of important landmarks to consider
|
||||||
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -43,7 +47,7 @@ class LandmarkManager:
|
|||||||
self.wikipedia_bonus = parameters['wikipedia_bonus']
|
self.wikipedia_bonus = parameters['wikipedia_bonus']
|
||||||
self.viewpoint_bonus = parameters['viewpoint_bonus']
|
self.viewpoint_bonus = parameters['viewpoint_bonus']
|
||||||
self.pay_bonus = parameters['pay_bonus']
|
self.pay_bonus = parameters['pay_bonus']
|
||||||
self.N_important = parameters['N_important']
|
self.n_important = parameters['N_important']
|
||||||
|
|
||||||
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
||||||
parameters = yaml.safe_load(f)
|
parameters = yaml.safe_load(f)
|
||||||
@ -113,7 +117,8 @@ class LandmarkManager:
|
|||||||
self.logger.debug('Fetching shopping clusters...')
|
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
|
||||||
all_landmarks.update(current_landmarks)
|
all_landmarks.update(current_landmarks)
|
||||||
|
|
||||||
# special pipeline for shopping malls
|
# special pipeline for shopping malls
|
||||||
@ -124,77 +129,12 @@ class LandmarkManager:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
landmarks_constrained = take_most_important(all_landmarks, self.N_important)
|
landmarks_constrained = take_most_important(all_landmarks, self.n_important)
|
||||||
# self.logger.info(f'All landmarks 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:
|
|
||||||
|
|
||||||
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
|
|
||||||
OpenStreetMap data to count the number of elements within that bounding box.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
coordinates (tuple[float, float]): The latitude and longitude of the location to search around.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
lat = coordinates[0]
|
|
||||||
lon = coordinates[1]
|
|
||||||
|
|
||||||
radius = self.radius_close_to
|
|
||||||
|
|
||||||
alpha = (180 * radius) / (6371000 * math.pi)
|
|
||||||
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
|
|
||||||
|
|
||||||
# Build the query to find elements within the radius
|
|
||||||
radius_query = overpassQueryBuilder(
|
|
||||||
bbox=[bbox['latLower'],
|
|
||||||
bbox['lonLower'],
|
|
||||||
bbox['latHigher'],
|
|
||||||
bbox['lonHigher']],
|
|
||||||
elementType=['node', 'way', 'relation']
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
radius_result = self.overpass.query(radius_query)
|
|
||||||
N_elem = radius_result.countWays() + radius_result.countRelations()
|
|
||||||
self.logger.debug(f"There are {N_elem} ways/relations within 50m")
|
|
||||||
if N_elem is None:
|
|
||||||
return 0
|
|
||||||
return N_elem
|
|
||||||
except:
|
|
||||||
return 0
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
|
|
||||||
# """
|
|
||||||
# Create a bounding box around the given coordinates.
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
|
|
||||||
# reachable_bbox_side (int): The side length of the bounding box in meters.
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
|
|
||||||
# defining the bounding box.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# # Half the side length in m (since it's a square bbox)
|
|
||||||
# half_side_length_m = reachable_bbox_side / 2
|
|
||||||
|
|
||||||
# return tuple((f"around:{half_side_length_m}", str(coordinates[0]), str(coordinates[1])))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]:
|
def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]:
|
||||||
"""
|
"""
|
||||||
Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates.
|
Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates.
|
||||||
@ -241,7 +181,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)
|
||||||
@ -274,7 +214,7 @@ class LandmarkManager:
|
|||||||
n_tags = len(elem.tags().keys()) # Add number of tags
|
n_tags = len(elem.tags().keys()) # Add number of tags
|
||||||
score = n_tags**self.tag_exponent # Add score
|
score = n_tags**self.tag_exponent # Add score
|
||||||
duration = 5 # Set base duration to 5 minutes
|
duration = 5 # Set base duration to 5 minutes
|
||||||
skip = False # Set skipping parameter to false
|
# skip = False # Set skipping parameter to false
|
||||||
tag_values = set(elem.tags().values()) # Store tag values
|
tag_values = set(elem.tags().values()) # Store tag values
|
||||||
|
|
||||||
|
|
||||||
@ -369,10 +309,10 @@ def dict_to_selector_list(d: dict) -> list:
|
|||||||
"""
|
"""
|
||||||
return_list = []
|
return_list = []
|
||||||
for key, value in d.items():
|
for key, value in d.items():
|
||||||
if type(value) == list:
|
if isinstance(value, list):
|
||||||
val = '|'.join(value)
|
val = '|'.join(value)
|
||||||
return_list.append(f'{key}~"^({val})$"')
|
return_list.append(f'{key}~"^({val})$"')
|
||||||
elif type(value) == str and len(value) == 0:
|
elif isinstance(value, str) and len(value) == 0:
|
||||||
return_list.append(f'{key}')
|
return_list.append(f'{key}')
|
||||||
else:
|
else:
|
||||||
return_list.append(f'{key}={value}')
|
return_list.append(f'{key}={value}')
|
||||||
|
@ -1,524 +0,0 @@
|
|||||||
import yaml, logging
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from scipy.optimize import linprog
|
|
||||||
from collections import defaultdict, deque
|
|
||||||
|
|
||||||
from ..structs.landmark import Landmark
|
|
||||||
from .get_time_separation import get_time
|
|
||||||
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Optimizer:
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
detour: int = None # accepted max detour time (in minutes)
|
|
||||||
detour_factor: float # detour factor of straight line vs real distance in cities
|
|
||||||
average_walking_speed: float # average walking speed of adult
|
|
||||||
max_landmarks: int # max number of landmarks to visit
|
|
||||||
overshoot: float # overshoot to allow maxtime to overflow. Optimizer is a bit restrictive
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self) :
|
|
||||||
|
|
||||||
# load parameters from file
|
|
||||||
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
|
|
||||||
parameters = yaml.safe_load(f)
|
|
||||||
self.detour_factor = parameters['detour_factor']
|
|
||||||
self.average_walking_speed = parameters['average_walking_speed']
|
|
||||||
self.max_landmarks = parameters['max_landmarks']
|
|
||||||
self.overshoot = parameters['overshoot']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Prevent the use of a particular solution
|
|
||||||
def prevent_config(self, resx):
|
|
||||||
"""
|
|
||||||
Prevent the use of a particular solution by adding constraints to the optimization.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
resx (list[float]): List of edge weights.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[list[int], list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
return h, [len(vertices_visited)-1]
|
|
||||||
|
|
||||||
|
|
||||||
# Prevents the creation of the same circle (both directions)
|
|
||||||
def prevent_circle(self, circle_vertices: list, L: int) :
|
|
||||||
"""
|
|
||||||
Prevent circular paths by by adding constraints to the optimization.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
circle_vertices (list): List of vertices forming a circle.
|
|
||||||
L (int): Number of landmarks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector.
|
|
||||||
"""
|
|
||||||
|
|
||||||
l1 = [0]*L*L
|
|
||||||
l2 = [0]*L*L
|
|
||||||
for i, node in enumerate(circle_vertices[:-1]) :
|
|
||||||
next = circle_vertices[i+1]
|
|
||||||
|
|
||||||
l1[node*L + next] = 1
|
|
||||||
l2[next*L + node] = 1
|
|
||||||
|
|
||||||
s = circle_vertices[0]
|
|
||||||
g = circle_vertices[-1]
|
|
||||||
|
|
||||||
l1[g*L + s] = 1
|
|
||||||
l2[s*L + g] = 1
|
|
||||||
|
|
||||||
return np.vstack((l1, l2)), [0, 0]
|
|
||||||
|
|
||||||
|
|
||||||
def is_connected(self, resx) :
|
|
||||||
"""
|
|
||||||
Determine the order of visits and detect any circular paths in the given configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
resx (list): List of edge weights.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
# Step 1: Create a graph representation
|
|
||||||
graph = defaultdict(list)
|
|
||||||
for a, b in zip(ind_a, ind_b):
|
|
||||||
graph[a].append(b)
|
|
||||||
|
|
||||||
# Step 2: Function to perform BFS/DFS to extract journeys
|
|
||||||
def get_journey(start):
|
|
||||||
journey_nodes = []
|
|
||||||
visited = set()
|
|
||||||
stack = deque([start])
|
|
||||||
|
|
||||||
while stack:
|
|
||||||
node = stack.pop()
|
|
||||||
if node not in visited:
|
|
||||||
visited.add(node)
|
|
||||||
journey_nodes.append(node)
|
|
||||||
for neighbor in graph[node]:
|
|
||||||
if neighbor not in visited:
|
|
||||||
stack.append(neighbor)
|
|
||||||
|
|
||||||
return journey_nodes
|
|
||||||
|
|
||||||
# Step 3: Extract all journeys
|
|
||||||
all_journeys_nodes = []
|
|
||||||
visited_nodes = set()
|
|
||||||
|
|
||||||
for node in ind_a:
|
|
||||||
if node not in visited_nodes:
|
|
||||||
journey_nodes = get_journey(node)
|
|
||||||
all_journeys_nodes.append(journey_nodes)
|
|
||||||
visited_nodes.update(journey_nodes)
|
|
||||||
|
|
||||||
for l in all_journeys_nodes :
|
|
||||||
if 0 in l :
|
|
||||||
order = l
|
|
||||||
all_journeys_nodes.remove(l)
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(all_journeys_nodes) == 0 :
|
|
||||||
return order, None
|
|
||||||
|
|
||||||
return order, all_journeys_nodes
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def init_ub_dist(self, landmarks: list[Landmark], max_time: int):
|
|
||||||
"""
|
|
||||||
Initialize the objective function coefficients and inequality constraints for the optimization problem.
|
|
||||||
|
|
||||||
This function computes the distances between all landmarks and stores their attractiveness to maximize sightseeing.
|
|
||||||
The goal is to maximize the objective function subject to the constraints A*x < b and A_eq*x = b_eq.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
landmarks (list[Landmark]): List of landmarks.
|
|
||||||
max_time (int): Maximum time of visit allowed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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_time(spot1.location, spot2.location) + spot1.duration
|
|
||||||
dist_table[j] = t
|
|
||||||
closest = sorted(dist_table)[:25]
|
|
||||||
for i, dist in enumerate(dist_table) :
|
|
||||||
if dist not in closest :
|
|
||||||
dist_table[i] = 32700
|
|
||||||
A_ub += dist_table
|
|
||||||
c = c*len(landmarks)
|
|
||||||
|
|
||||||
return c, A_ub, [max_time*self.overshoot]
|
|
||||||
|
|
||||||
|
|
||||||
def respect_number(self, L, max_landmarks: int):
|
|
||||||
"""
|
|
||||||
Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks.
|
|
||||||
|
|
||||||
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 = [1]*L
|
|
||||||
zeros = [0]*L
|
|
||||||
A = ones + zeros*(L-1)
|
|
||||||
b = [1]
|
|
||||||
for i in range(L-1) :
|
|
||||||
h_new = zeros*i + ones + zeros*(L-1-i)
|
|
||||||
A = np.vstack((A, h_new))
|
|
||||||
b.append(1)
|
|
||||||
|
|
||||||
A = np.vstack((A, ones*L))
|
|
||||||
b.append(max_landmarks+1)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
# Constraint to not have d14 and d41 simultaneously. Does not prevent cyclic paths with more elements
|
|
||||||
def break_sym(self, L):
|
|
||||||
"""
|
|
||||||
Generate constraints to prevent simultaneous travel between two landmarks in both directions.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
L (int): Number of landmarks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
upper_ind = np.triu_indices(L,0,L)
|
|
||||||
|
|
||||||
up_ind_x = upper_ind[0]
|
|
||||||
up_ind_y = upper_ind[1]
|
|
||||||
|
|
||||||
A = [0]*L*L
|
|
||||||
b = [1]
|
|
||||||
|
|
||||||
for i, _ in enumerate(up_ind_x[1:]) :
|
|
||||||
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 = np.vstack((A,l))
|
|
||||||
b.append(1)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
def init_eq_not_stay(self, L: int):
|
|
||||||
"""
|
|
||||||
Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
L (int): Number of landmarks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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), dtype=np.int8)
|
|
||||||
|
|
||||||
return [l], [0]
|
|
||||||
|
|
||||||
|
|
||||||
def respect_user_must_do(self, landmarks: list[Landmark]) :
|
|
||||||
"""
|
|
||||||
Generate constraints to ensure that landmarks marked as 'must_do' are included in the optimization.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
L = len(landmarks)
|
|
||||||
A = [0]*L*L
|
|
||||||
b = [0]
|
|
||||||
|
|
||||||
for i, elem in enumerate(landmarks[1:]) :
|
|
||||||
if elem.must_do is True and elem.name not in ['finish', 'start']:
|
|
||||||
l = [0]*L*L
|
|
||||||
l[i*L:i*L+L] = [1]*L # set mandatory departures from landmarks tagged as 'must_do'
|
|
||||||
|
|
||||||
A = np.vstack((A,l))
|
|
||||||
b.append(1)
|
|
||||||
|
|
||||||
return A, 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.
|
|
||||||
|
|
||||||
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 = [0]*L*L
|
|
||||||
b = [0]
|
|
||||||
|
|
||||||
for i, elem in enumerate(landmarks[1:]) :
|
|
||||||
if elem.must_avoid is True and elem.name not in ['finish', 'start']:
|
|
||||||
l = [0]*L*L
|
|
||||||
l[i*L:i*L+L] = [1]*L
|
|
||||||
|
|
||||||
A = np.vstack((A,l))
|
|
||||||
b.append(0) # prevent departures from landmarks tagged as 'must_do'
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
# Constraint to ensure start at start and finish at goal
|
|
||||||
def respect_start_finish(self, L: int):
|
|
||||||
"""
|
|
||||||
Generate constraints to ensure that the optimization starts at the designated start landmark and finishes at the goal landmark.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
L (int): Number of landmarks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
l_start = [1]*L + [0]*L*(L-1) # sets departures only for start (horizontal ones)
|
|
||||||
l_start[L-1] = 0 # prevents the jump from start to finish
|
|
||||||
l_goal = [0]*L*L # sets arrivals only for finish (vertical ones)
|
|
||||||
l_L = [0]*L*(L-1) + [1]*L # prevents arrivals at start and departures from goal
|
|
||||||
for k in range(L-1) : # sets only vertical ones for goal (go to)
|
|
||||||
l_L[k*L] = 1
|
|
||||||
if k != 0 :
|
|
||||||
l_goal[k*L+L-1] = 1
|
|
||||||
|
|
||||||
A = np.vstack((l_start, l_goal))
|
|
||||||
b = [1, 1]
|
|
||||||
A = np.vstack((A,l_L))
|
|
||||||
b.append(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.
|
|
||||||
|
|
||||||
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 = [0]*L*L
|
|
||||||
b = [0]
|
|
||||||
for i in range(L-1) : # Prevent stacked ones
|
|
||||||
if i == 0 or i == L-1: # Don't touch start or finish
|
|
||||||
continue
|
|
||||||
else :
|
|
||||||
l = [0]*L
|
|
||||||
l[i] = -1
|
|
||||||
l = l*L
|
|
||||||
for j in range(L) :
|
|
||||||
l[i*L + j] = 1
|
|
||||||
|
|
||||||
A = np.vstack((A,l))
|
|
||||||
b.append(0)
|
|
||||||
|
|
||||||
return A, b
|
|
||||||
|
|
||||||
|
|
||||||
def link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] :
|
|
||||||
"""
|
|
||||||
Compute the time to reach from each landmark to the next and create a list of landmarks with updated travel times.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
order (list[int]): List of indices representing the order of landmarks to visit.
|
|
||||||
landmarks (list[Landmark]): List of all landmarks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Landmark]]: The updated linked list of landmarks with travel times
|
|
||||||
"""
|
|
||||||
|
|
||||||
L = []
|
|
||||||
j = 0
|
|
||||||
while j < len(order)-1 :
|
|
||||||
# get landmarks involved
|
|
||||||
elem = landmarks[order[j]]
|
|
||||||
next = landmarks[order[j+1]]
|
|
||||||
|
|
||||||
# get attributes
|
|
||||||
elem.time_to_reach_next = get_time(elem.location, next.location)
|
|
||||||
elem.must_do = True
|
|
||||||
elem.location = (round(elem.location[0], 5), round(elem.location[1], 5))
|
|
||||||
elem.next_uuid = next.uuid
|
|
||||||
L.append(elem)
|
|
||||||
j += 1
|
|
||||||
|
|
||||||
next.location = (round(next.location[0], 5), round(next.location[1], 5))
|
|
||||||
next.must_do = True
|
|
||||||
L.append(next)
|
|
||||||
|
|
||||||
return L
|
|
||||||
|
|
||||||
|
|
||||||
# Main optimization pipeline
|
|
||||||
def solve_optimization(
|
|
||||||
self,
|
|
||||||
max_time: int,
|
|
||||||
landmarks: list[Landmark],
|
|
||||||
max_landmarks: int = None
|
|
||||||
) -> list[Landmark]:
|
|
||||||
"""
|
|
||||||
Main optimization pipeline to solve the landmark visiting problem.
|
|
||||||
|
|
||||||
This method sets up and solves a linear programming problem with constraints to find an optimal tour of landmarks,
|
|
||||||
considering user-defined must-visit landmarks, start and finish points, and ensuring no cycles are present.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
max_time (int): Maximum time allowed for the tour in minutes.
|
|
||||||
landmarks (list[Landmark]): List of landmarks to visit.
|
|
||||||
max_landmarks (int): Maximum number of landmarks visited
|
|
||||||
Returns:
|
|
||||||
list[Landmark]: The optimized tour of landmarks with updated travel times, or None if no valid solution is found.
|
|
||||||
"""
|
|
||||||
if max_landmarks is None :
|
|
||||||
max_landmarks = self.max_landmarks
|
|
||||||
|
|
||||||
L = len(landmarks)
|
|
||||||
|
|
||||||
# SET CONSTRAINTS FOR INEQUALITY
|
|
||||||
c, A_ub, b_ub = self.init_ub_dist(landmarks, max_time) # Add the distances from each landmark to the other
|
|
||||||
A, b = self.respect_number(L, max_landmarks) # Respect max number of visits (no more possible stops than landmarks).
|
|
||||||
A_ub = np.vstack((A_ub, A), dtype=np.int16)
|
|
||||||
b_ub += b
|
|
||||||
A, b = self.break_sym(L) # break the 'zig-zag' symmetry
|
|
||||||
A_ub = np.vstack((A_ub, A), dtype=np.int16)
|
|
||||||
b_ub += b
|
|
||||||
|
|
||||||
|
|
||||||
# SET CONSTRAINTS FOR EQUALITY
|
|
||||||
A_eq, b_eq = self.init_eq_not_stay(L) # 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
|
|
||||||
A_eq = np.vstack((A_eq, A), dtype=np.int8)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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. Try with a longer trip (>30 minutes).")
|
|
||||||
|
|
||||||
# If there is a solution, we're good to go, just check for connectiveness
|
|
||||||
order, circles = self.is_connected(res.x)
|
|
||||||
#nodes, edges = is_connected(res.x)
|
|
||||||
i = 0
|
|
||||||
timeout = 80
|
|
||||||
while circles is not None and i < timeout:
|
|
||||||
A, b = self.prevent_config(res.x)
|
|
||||||
A_ub = np.vstack((A_ub, A))
|
|
||||||
b_ub += b
|
|
||||||
#A_ub, b_ub = prevent_circle(order, len(landmarks), A_ub, b_ub)
|
|
||||||
for circle in circles :
|
|
||||||
A, b = self.prevent_circle(circle, L)
|
|
||||||
A_eq = np.vstack((A_eq, A))
|
|
||||||
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)
|
|
||||||
if not res.success :
|
|
||||||
raise ArithmeticError("Solving failed because of overconstrained problem")
|
|
||||||
return None
|
|
||||||
order, circles = self.is_connected(res.x)
|
|
||||||
#nodes, edges = is_connected(res.x)
|
|
||||||
if circles is None :
|
|
||||||
break
|
|
||||||
# print(i)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if i == timeout :
|
|
||||||
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
|
||||||
|
|
||||||
#sort the landmarks in the order of the solution
|
|
||||||
tour = [landmarks[i] for i in order]
|
|
||||||
|
|
||||||
self.logger.debug(f"Re-optimized {i} times, score: {int(-res.fun)}")
|
|
||||||
return tour
|
|
@ -1,19 +1,43 @@
|
|||||||
import yaml, logging
|
"""Module responsible for sloving an MILP to find best tour around the given landmarks."""
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
import yaml
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pulp as pl
|
import pulp as pl
|
||||||
from scipy.optimize import linprog
|
|
||||||
from collections import defaultdict, deque
|
|
||||||
|
|
||||||
from ..structs.landmark import Landmark
|
from ..structs.landmark import Landmark
|
||||||
from .get_time_separation import get_time
|
from .get_time_separation import get_time
|
||||||
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
||||||
|
|
||||||
|
|
||||||
|
# Silence the pupl logger
|
||||||
logging.getLogger('pulp').setLevel(level=logging.CRITICAL)
|
logging.getLogger('pulp').setLevel(level=logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
class Optimizer:
|
class Optimizer:
|
||||||
|
"""
|
||||||
|
Optimizes the balance between the efficiency of a tour and the inclusion of landmarks.
|
||||||
|
|
||||||
|
The `Optimizer` class is responsible for calculating the best possible detour adjustments
|
||||||
|
to a tour based on specific parameters such as detour time, walking speed, and the maximum
|
||||||
|
number of landmarks to visit. It helps refine a tour by determining whether adding additional
|
||||||
|
landmarks would significantly reduce the overall efficiency.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
- Calculates the maximum detour time allowed for a given tour.
|
||||||
|
- Considers the detour factor, which accounts for real-world walking paths versus straight-line distance.
|
||||||
|
- Takes into account the average walking speed to estimate walking times.
|
||||||
|
- Limits the number of landmarks that can be added to the tour to prevent excessive detouring.
|
||||||
|
- Allows some overflow (overshoot) in the maximum detour time to accommodate for slight inefficiencies.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
logger (logging.Logger): Logger for capturing relevant events and errors.
|
||||||
|
detour (int): The accepted maximum detour time in minutes.
|
||||||
|
detour_factor (float): The ratio between straight-line distance and actual walking distance in cities.
|
||||||
|
average_walking_speed (float): The average walking speed of an adult (in meters per second or kilometers per hour).
|
||||||
|
max_landmarks (int): The maximum number of landmarks to include in the tour.
|
||||||
|
overshoot (float): The overshoot allowance for exceeding the maximum detour time in a restrictive manner.
|
||||||
|
"""
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
detour: int = None # accepted max detour time (in minutes)
|
detour: int = None # accepted max detour time (in minutes)
|
||||||
@ -469,6 +493,33 @@ class Optimizer:
|
|||||||
|
|
||||||
|
|
||||||
def pre_processing(self, L: int, landmarks: list[Landmark], max_time: int, max_landmarks: int | None) :
|
def pre_processing(self, L: int, landmarks: list[Landmark], max_time: int, max_landmarks: int | None) :
|
||||||
|
"""
|
||||||
|
Preprocesses the optimization problem by setting up constraints and variables for the tour optimization.
|
||||||
|
|
||||||
|
This method initializes and prepares the linear programming problem to optimize a tour that includes landmarks,
|
||||||
|
while respecting various constraints such as time limits, the number of landmarks to visit, and user preferences.
|
||||||
|
The pre-processing step sets up the problem before solving it using a linear programming solver.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
- Defines the optimization problem using linear programming (LP) with the objective to maximize the tour value.
|
||||||
|
- Creates binary decision variables for each potential transition between landmarks.
|
||||||
|
- Sets up inequality constraints to respect the maximum time available for the tour and the maximum number of landmarks.
|
||||||
|
- Implements equality constraints to ensure the tour respects the start and finish positions, avoids staying in the same place,
|
||||||
|
and adheres to a visit order.
|
||||||
|
- Forces inclusion or exclusion of specific landmarks based on user preferences.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
prob (pl.LpProblem): The linear programming problem to be solved.
|
||||||
|
x (list): A list of binary variables representing transitions between landmarks.
|
||||||
|
L (int): The total number of landmarks considered in the optimization.
|
||||||
|
landmarks (list[Landmark]): The list of landmarks to be visited in the tour.
|
||||||
|
max_time (int): The maximum allowable time for the entire tour.
|
||||||
|
max_landmarks (int | None): The maximum number of landmarks to visit in the tour, or None if no limit is set.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
prob (pl.LpProblem): The linear programming problem setup for optimization.
|
||||||
|
x (list): The list of binary variables for transitions between landmarks in the tour.
|
||||||
|
"""
|
||||||
|
|
||||||
if max_landmarks is None :
|
if max_landmarks is None :
|
||||||
max_landmarks = self.max_landmarks
|
max_landmarks = self.max_landmarks
|
||||||
|
@ -13,7 +13,14 @@ from ..constants import OPTIMIZER_PARAMETERS_PATH
|
|||||||
|
|
||||||
|
|
||||||
class Refiner :
|
class Refiner :
|
||||||
|
"""
|
||||||
|
Refines a tour by incorporating smaller landmarks along the path to enhance the experience.
|
||||||
|
|
||||||
|
This class is designed to adjust an existing tour by considering additional,
|
||||||
|
smaller points of interest (landmarks) that may require minor detours but
|
||||||
|
improve the overall quality of the tour. It balances the efficiency of travel
|
||||||
|
with the added value of visiting these landmarks.
|
||||||
|
"""
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
detour_factor: float # detour factor of straight line vs real distance in cities
|
detour_factor: float # detour factor of straight line vs real distance in cities
|
||||||
@ -267,7 +274,7 @@ class Refiner :
|
|||||||
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
||||||
xs, ys = better_tour_poly.exterior.xy
|
xs, ys = better_tour_poly.exterior.xy
|
||||||
|
|
||||||
except :
|
except Exception:
|
||||||
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
|
||||||
xs, ys = better_tour_poly.exterior.xy
|
xs, ys = better_tour_poly.exterior.xy
|
||||||
"""
|
"""
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
"""Helper function to return only the major landmarks from a large list."""
|
||||||
from ..structs.landmark import Landmark
|
from ..structs.landmark import Landmark
|
||||||
|
|
||||||
def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]:
|
def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]:
|
||||||
|
@ -1,16 +1,34 @@
|
|||||||
import logging, yaml
|
"""Module for finding public toilets around given coordinates."""
|
||||||
|
import logging
|
||||||
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
|
||||||
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
|
||||||
|
|
||||||
from ..structs.landmark import Toilets
|
from ..structs.landmark import Toilets
|
||||||
from ..constants import LANDMARK_PARAMETERS_PATH, OSM_CACHE_DIR
|
from ..constants import OSM_CACHE_DIR
|
||||||
|
|
||||||
|
|
||||||
# silence the overpass logger
|
# silence the overpass logger
|
||||||
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
|
logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL)
|
||||||
|
|
||||||
class ToiletsManager:
|
class ToiletsManager:
|
||||||
|
"""
|
||||||
|
Manages the process of fetching and caching toilet information from
|
||||||
|
OpenStreetMap (OSM) based on a specified location and radius.
|
||||||
|
|
||||||
|
This class is responsible for:
|
||||||
|
- Fetching toilet data from OSM using Overpass API around a given set of
|
||||||
|
coordinates (latitude, longitude).
|
||||||
|
- Using a caching strategy to optimize requests by saving and retrieving
|
||||||
|
data from a local cache.
|
||||||
|
- Logging important events and errors related to data fetching.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
logger (logging.Logger): Logger for the class to capture events.
|
||||||
|
location (tuple[float, float]): Latitude and longitude representing the
|
||||||
|
location to search around.
|
||||||
|
radius (int): The search radius in meters for finding nearby toilets.
|
||||||
|
overpass (Overpass): The Overpass API instance used to query OSM.
|
||||||
|
"""
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
location: tuple[float, float]
|
location: tuple[float, float]
|
||||||
@ -26,9 +44,14 @@ class ToiletsManager:
|
|||||||
|
|
||||||
|
|
||||||
def generate_toilet_list(self) -> list[Toilets] :
|
def generate_toilet_list(self) -> list[Toilets] :
|
||||||
|
"""
|
||||||
|
Generates a list of toilet locations by fetching data from OpenStreetMap (OSM)
|
||||||
|
around the given coordinates stored in `self.location`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
# Create a bbox using the around technique
|
list[Toilets]: A list of `Toilets` objects containing detailed information
|
||||||
|
about the toilets found around the given coordinates.
|
||||||
|
"""
|
||||||
bbox = tuple((f"around:{self.radius}", str(self.location[0]), str(self.location[1])))
|
bbox = tuple((f"around:{self.radius}", str(self.location[0]), str(self.location[1])))
|
||||||
toilets_list = []
|
toilets_list = []
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user